From 748a71139695422eccf253112321a7a5f6b9b83d Mon Sep 17 00:00:00 2001 From: Ryan Liang <109499885+RyanL1997@users.noreply.github.com> Date: Thu, 29 Jun 2023 12:37:08 -0700 Subject: [PATCH] Feature branch sync 06/29/2023 (#2918) * add search model group permission to ml_read_access role (#2855) * add search model group permission to ml_read_access role Signed-off-by: Bhavana Ramaram * IntegrationTest spotless (#2863) Signed-off-by: Stephen Crawford * Format everything (#2866) * Use boucycastle PEM reader instead of reg expression (#2864) Use BouncyCastle PEMReader instead of regular expression to read and parse private key pem files. Signed-off-by: Andrey Pleskach * Adding field level security test cases for FlatFields (#2876) Signed-off-by: Peter Nied * Update snappy to 1.1.10.1 and guava to 32.0.1-jre (#2886) * Update snappy to 1.1.10.1 and guava to 32.0.1-jre Signed-off-by: Craig Perkins * Upgrade kafka to 3.5.0 Signed-off-by: Craig Perkins * Force snappy Signed-off-by: Craig Perkins * Add runtime dependency on org.scala-lang.modules:scala-java8-compat_3:1.0.2 to fix issue with KafkaSinkTest Signed-off-by: Craig Perkins --------- Signed-off-by: Craig Perkins * Role permissions order tool and workflow (#2733) * Check Permissions Order tool and workflow Adds a NodeJS tool that can inspect yaml role definitions, check if they are in alphabetical order, correct them if required. Signed-off-by: Peter Nied * Apply fixes to roles.yml files Signed-off-by: Peter Nied * Fixing busted test, adding findArrayInJson for response bodies Signed-off-by: Peter Nied --------- Signed-off-by: Peter Nied Signed-off-by: Peter Nied * Misc changes (#2902) Moved isStatic and isReserved methods to the SecurityDynamicConfiguration class Signed-off-by: Andrey Pleskach * Update triaging guidelines (#2899) * Update triaging guidelines Signed-off-by: Stephen Crawford Signed-off-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> * fix cluster perm classification for msearch template (#2892) * fix cluster perm classification for msearch template Signed-off-by: Derek Ho * move test to unit test file Signed-off-by: Derek Ho * fully revert integration test file Signed-off-by: Derek Ho * Update src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java Signed-off-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> * spotless Signed-off-by: Derek Ho --------- Signed-off-by: Derek Ho Signed-off-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Co-authored-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> * [Doc] Add architecture document (#2869) * Add initial architecture document Signed-off-by: Peter Nied * [Enhancement] Parallel test jobs for CI (#2861) * Split multiple tests into separate gradle tasks. * Tasks are configured in "splitTestConfig" map in build.gradle file. Map allows to use all patterns from TestFilter like: includeTestsMatching, excludeTestsMatching, includeTest etc. * Tasks are automatically generated from "splitTestConfig" map. * Two new Gradle tasks: listTasksAsJSON and listTasksAsParam to output task names to console. First one outputs them as a JSON and second - in gradlew "-x " format to use in CLI. * Patterns included in tasks are automatically excluded from main "test" task but at the same time generated tasks are dependencies for "test". Running "gradlew test" will run whole suite at once. * CI pipeline has been configured to accomodate all changes. * New 'master' task to generate list of jobs to run in parallel. * Updated matrix strategy to include task name to start. Signed-off-by: Pawel Gudel * Bump BouncyCastle from jdk15on to jdk15to18 (#2901) jdk15to18 contains fix for - CVE-2023-33201 - Medium Severity Vulnerability Signed-off-by: Andrey Pleskach * Spotless Apply Signed-off-by: Ryan Liang --------- Signed-off-by: Bhavana Ramaram Signed-off-by: Stephen Crawford Signed-off-by: Andrey Pleskach Signed-off-by: Peter Nied Signed-off-by: Craig Perkins Signed-off-by: Peter Nied Signed-off-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Signed-off-by: Derek Ho Signed-off-by: Pawel Gudel Signed-off-by: Ryan Liang Co-authored-by: Bhavana Ramaram Co-authored-by: Stephen Crawford <65832608+scrawfor99@users.noreply.github.com> Co-authored-by: Andrey Pleskach Co-authored-by: Peter Nied Co-authored-by: Craig Perkins Co-authored-by: Derek Ho Co-authored-by: pawel-gudel-eliatra <136344230+pawel-gudel-eliatra@users.noreply.github.com> --- .github/workflows/ci.yml | 44 +- .github/workflows/code-hygiene.yml | 23 + .gitignore | 4 + ARCHITECTURE.md | 131 + TRIAGING.md | 5 +- build.gradle | 221 +- check-permissions-order.js | 86 + config/roles.yml | 60 +- gradle/formatting.gradle | 111 +- plugin-security.policy | 7 +- .../logging/NodeAndClusterIdConverter.java | 10 +- .../org/opensearch/node/PluginAwareNode.java | 27 +- .../security/ConfigurationFiles.java | 76 +- .../security/CrossClusterSearchTests.java | 748 +-- .../security/DefaultConfigurationTests.java | 72 +- .../security/DlsIntegrationTests.java | 867 +-- .../security/DoNotFailOnForbiddenTests.java | 623 +-- .../security/FlsAndFieldMaskingTests.java | 1375 ++--- .../security/IndexOperationsHelper.java | 58 +- .../IpBruteForceAttacksPreventionTests.java | 246 +- .../security/PointInTimeOperationTest.java | 687 +-- .../security/SearchOperationTest.java | 4664 +++++++++-------- .../security/SecurityAdminLauncher.java | 59 +- .../security/SecurityConfigurationTests.java | 375 +- .../security/SecurityRolesTests.java | 51 +- .../opensearch/security/SnapshotSteps.java | 96 +- .../java/org/opensearch/security/Song.java | 167 +- .../org/opensearch/security/SslOnlyTests.java | 54 +- .../org/opensearch/security/TlsTests.java | 104 +- .../UserBruteForceAttacksPreventionTests.java | 182 +- .../http/AnonymousAuthenticationTest.java | 183 +- .../opensearch/security/http/AuthInfo.java | 16 +- .../security/http/BasicAuthTests.java | 212 +- .../http/BasicAuthWithoutChallengeTests.java | 41 +- .../http/CertificateAuthenticationTest.java | 207 +- .../http/CommonProxyAuthenticationTests.java | 445 +- .../http/DirectoryInformationTrees.java | 205 +- .../security/http/DisabledBasicAuthTests.java | 33 +- .../http/ExtendedProxyAuthenticationTest.java | 405 +- .../security/http/JwtAuthenticationTests.java | 395 +- .../http/JwtAuthorizationHeaderFactory.java | 224 +- .../security/http/LdapAuthenticationTest.java | 122 +- .../http/LdapStartTlsAuthenticationTest.java | 101 +- .../http/LdapTlsAuthenticationTest.java | 627 +-- .../http/ProxyAuthenticationTest.java | 177 +- .../UntrustedLdapServerCertificateTest.java | 79 +- .../privileges/PrivilegesEvaluatorTest.java | 50 +- src/integrationTest/resources/roles.yml | 20 +- .../security/DefaultObjectMapper.java | 38 +- .../security/OpenSearchSecurityPlugin.java | 1242 ++++- .../ConfigUpdateNodeResponse.java | 4 +- .../configupdate/ConfigUpdateRequest.java | 4 +- .../ConfigUpdateRequestBuilder.java | 6 +- .../configupdate/ConfigUpdateResponse.java | 20 +- .../TransportConfigUpdateAction.java | 45 +- .../action/whoami/TransportWhoAmIAction.java | 26 +- .../action/whoami/WhoAmIRequestBuilder.java | 3 +- .../action/whoami/WhoAmIResponse.java | 3 +- .../security/auditlog/AuditLog.java | 22 +- .../auditlog/AuditLogSslExceptionHandler.java | 42 +- .../security/auditlog/NullAuditLog.java | 32 +- .../security/auditlog/config/AuditConfig.java | 237 +- .../auditlog/config/ThreadPoolConfig.java | 9 +- .../auditlog/impl/AbstractAuditLog.java | 440 +- .../security/auditlog/impl/AuditCategory.java | 9 +- .../security/auditlog/impl/AuditLogImpl.java | 332 +- .../security/auditlog/impl/AuditMessage.java | 154 +- .../auditlog/impl/RequestResolver.java | 299 +- .../auditlog/routing/AsyncStoragePool.java | 121 +- .../auditlog/routing/AuditMessageRouter.java | 25 +- .../security/auditlog/sink/AuditLogSink.java | 74 +- .../auditlog/sink/ExternalOpenSearchSink.java | 281 +- .../auditlog/sink/InternalOpenSearchSink.java | 97 +- .../security/auditlog/sink/KafkaSink.java | 108 +- .../security/auditlog/sink/Log4JSink.java | 8 +- .../security/auditlog/sink/NoopSink.java | 2 +- .../security/auditlog/sink/SinkProvider.java | 308 +- .../security/auditlog/sink/WebhookSink.java | 701 +-- .../opensearch/security/auth/AuthDomain.java | 11 +- .../security/auth/BackendRegistry.java | 312 +- .../opensearch/security/auth/Destroyable.java | 2 +- .../security/auth/RolesInjector.java | 20 +- .../security/auth/UserInjector.java | 6 +- .../auth/blocking/ClientBlockRegistry.java | 2 + .../HeapBasedClientBlockRegistry.java | 20 +- .../InternalAuthenticationBackend.java | 43 +- .../auth/limiting/AbstractRateLimiter.java | 14 +- .../limiting/AddressBasedRateLimiter.java | 5 +- .../jwt/EncryptionDecryptionUtil.java | 3 +- .../security/authtoken/jwt/JwtVendor.java | 23 +- .../security/compliance/ComplianceConfig.java | 253 +- .../ComplianceIndexingOperationListener.java | 2 +- ...mplianceIndexingOperationListenerImpl.java | 37 +- .../compliance/FieldReadCallback.java | 54 +- .../security/configuration/AdminDNs.java | 30 +- .../configuration/ClusterInfoHolder.java | 10 +- .../security/configuration/CompatConfig.java | 24 +- .../configuration/ConfigCallback.java | 3 + .../ConfigurationLoaderSecurity7.java | 118 +- .../ConfigurationRepository.java | 215 +- .../DlsFilterLevelActionHandler.java | 146 +- .../configuration/DlsFlsFilterLeafReader.java | 215 +- .../configuration/DlsFlsRequestValve.java | 19 +- .../configuration/DlsFlsValveImpl.java | 202 +- .../configuration/DlsQueryParser.java | 52 +- .../configuration/EmptyFilterLeafReader.java | 1 + .../security/configuration/MaskedField.java | 81 +- .../PrivilegesInterceptorImpl.java | 84 +- .../security/configuration/Salt.java | 15 +- .../SecurityFlsDlsIndexSearcherWrapper.java | 74 +- .../SecurityIndexSearcherWrapper.java | 30 +- .../dlic/rest/api/AbstractApiAction.java | 1116 ++-- .../dlic/rest/api/AccountApiAction.java | 78 +- .../dlic/rest/api/ActionGroupsApiAction.java | 220 +- .../dlic/rest/api/AllowlistApiAction.java | 71 +- .../dlic/rest/api/AuditApiAction.java | 53 +- .../rest/api/AuthTokenProcessorAction.java | 128 +- .../dlic/rest/api/FlushCacheApiAction.java | 209 +- .../dlic/rest/api/InternalUsersApiAction.java | 106 +- .../dlic/rest/api/MigrateApiAction.java | 178 +- .../rest/api/MultiTenancyConfigApiAction.java | 138 +- .../dlic/rest/api/NodesDnApiAction.java | 32 +- .../rest/api/PatchableResourceApiAction.java | 110 +- .../dlic/rest/api/PermissionsInfoAction.java | 131 +- .../api/RestApiAdminPrivilegesEvaluator.java | 106 +- .../rest/api/RestApiPrivilegesEvaluator.java | 811 +-- .../dlic/rest/api/RolesApiAction.java | 111 +- .../dlic/rest/api/RolesMappingApiAction.java | 213 +- .../dlic/rest/api/SecurityConfigAction.java | 59 +- .../dlic/rest/api/SecurityRestApiActions.java | 321 +- .../dlic/rest/api/SecuritySSLCertsAction.java | 164 +- .../dlic/rest/api/TenantsApiAction.java | 30 +- .../dlic/rest/api/ValidateApiAction.java | 71 +- .../dlic/rest/api/WhitelistApiAction.java | 40 +- .../security/dlic/rest/support/Utils.java | 33 +- .../AbstractConfigurationValidator.java | 45 +- .../rest/validation/ActionGroupValidator.java | 24 +- .../dlic/rest/validation/AuditValidator.java | 37 +- .../rest/validation/CredentialsValidator.java | 11 +- .../validation/InternalUsersValidator.java | 9 +- .../MultiTenancyConfigValidator.java | 1 - .../dlic/rest/validation/NoOpValidator.java | 6 +- .../rest/validation/PasswordValidator.java | 72 +- .../validation/RolesMappingValidator.java | 34 +- .../dlic/rest/validation/RolesValidator.java | 26 +- .../validation/SecurityConfigValidator.java | 10 +- .../dlic/rest/validation/TenantValidator.java | 8 +- .../security/filter/SecurityFilter.java | 276 +- .../security/filter/SecurityRestFilter.java | 46 +- .../security/http/HTTPBasicAuthenticator.java | 2 +- .../http/HTTPClientCertAuthenticator.java | 8 +- .../http/HTTPOnBehalfOfJwtAuthenticator.java | 53 +- .../security/http/HTTPProxyAuthenticator.java | 13 +- .../security/http/RemoteIpDetector.java | 58 +- .../http/SecurityHttpServerTransport.java | 28 +- .../SecurityNonSslHttpServerTransport.java | 14 +- .../opensearch/security/http/XFFResolver.java | 33 +- .../proxy/HTTPExtendedProxyAuthenticator.java | 18 +- .../security/httpclient/HttpClient.java | 109 +- .../privileges/PitPrivilegesEvaluator.java | 65 +- .../privileges/PrivilegesEvaluator.java | 267 +- .../PrivilegesEvaluatorResponse.java | 22 +- .../privileges/PrivilegesInterceptor.java | 19 +- .../ProtectedIndexAccessEvaluator.java | 48 +- .../SecurityIndexAccessEvaluator.java | 72 +- .../privileges/SnapshotRestoreEvaluator.java | 21 +- .../privileges/TermsAggregationEvaluator.java | 90 +- .../queries/QueryBuilderTraverser.java | 17 +- .../resolver/IndexResolverReplacer.java | 325 +- .../security/rest/DashboardsInfoAction.java | 34 +- .../rest/SecurityConfigUpdateAction.java | 25 +- .../security/rest/SecurityHealthAction.java | 14 +- .../security/rest/SecurityInfoAction.java | 64 +- .../security/rest/SecurityWhoAmIAction.java | 167 +- .../security/rest/TenantInfoAction.java | 114 +- .../security/securityconf/ConfigModel.java | 3 +- .../security/securityconf/ConfigModelV6.java | 537 +- .../security/securityconf/ConfigModelV7.java | 609 ++- .../securityconf/DynamicConfigFactory.java | 235 +- .../securityconf/DynamicConfigModel.java | 26 +- .../securityconf/DynamicConfigModelV6.java | 158 +- .../securityconf/DynamicConfigModelV7.java | 162 +- .../securityconf/EvaluatedDlsFlsConfig.java | 29 +- .../security/securityconf/Hashed.java | 2 + .../security/securityconf/Hideable.java | 1 + .../securityconf/InternalUsersModel.java | 5 + .../security/securityconf/Migration.java | 80 +- .../security/securityconf/RoleMappings.java | 4 +- .../security/securityconf/SecurityRoles.java | 40 +- .../impl/AllowlistingSettings.java | 45 +- .../security/securityconf/impl/CType.java | 6 +- .../security/securityconf/impl/Meta.java | 4 +- .../impl/SecurityDynamicConfiguration.java | 126 +- .../impl/WhitelistingSettings.java | 45 +- .../securityconf/impl/v6/ActionGroupsV6.java | 9 +- .../securityconf/impl/v6/ConfigV6.java | 112 +- .../securityconf/impl/v6/InternalUserV6.java | 188 +- .../securityconf/impl/v6/RoleMappingsV6.java | 28 +- .../security/securityconf/impl/v6/RoleV6.java | 16 +- .../securityconf/impl/v7/ActionGroupsV7.java | 36 +- .../securityconf/impl/v7/ConfigV7.java | 156 +- .../securityconf/impl/v7/InternalUserV7.java | 284 +- .../securityconf/impl/v7/RoleMappingsV7.java | 42 +- .../security/securityconf/impl/v7/RoleV7.java | 78 +- .../securityconf/impl/v7/TenantV7.java | 9 +- .../security/setting/DeprecatedSettings.java | 7 +- .../setting/OpensearchDynamicSetting.java | 9 +- .../setting/TransportPassiveAuthSetting.java | 10 +- .../security/ssl/DefaultSecurityKeyStore.java | 749 +-- .../ssl/ExternalSecurityKeyStore.java | 22 +- .../ssl/OpenSearchSecuritySSLPlugin.java | 455 +- .../security/ssl/SecureSSLSettings.java | 21 +- .../security/ssl/SecurityKeyStore.java | 6 + .../security/ssl/SslExceptionHandler.java | 6 +- .../SecuritySSLNettyHttpServerTransport.java | 22 +- .../ssl/http/netty/ValidatingDispatcher.java | 13 +- .../ssl/rest/SecuritySSLInfoAction.java | 41 +- .../transport/DefaultPrincipalExtractor.java | 9 +- .../ssl/transport/DualModeSSLHandler.java | 1 - .../security/ssl/transport/SSLConfig.java | 15 +- .../transport/SecuritySSLNettyTransport.java | 82 +- .../transport/SecuritySSLRequestHandler.java | 83 +- .../SecuritySSLTransportInterceptor.java | 19 +- .../security/ssl/util/CertFileProps.java | 55 +- .../security/ssl/util/CertFromFile.java | 7 +- .../security/ssl/util/CertFromKeystore.java | 32 +- .../security/ssl/util/CertFromTruststore.java | 6 +- .../ssl/util/CertificateValidator.java | 99 +- .../security/ssl/util/ExceptionUtils.java | 23 +- .../ssl/util/SSLCertificateHelper.java | 44 +- .../security/ssl/util/SSLConfigConstants.java | 167 +- .../ssl/util/SSLConnectionTestUtil.java | 29 +- .../security/ssl/util/SSLRequestHelper.java | 83 +- .../opensearch/security/ssl/util/TLSUtil.java | 6 +- .../opensearch/security/ssl/util/Utils.java | 7 +- .../security/support/Base64Helper.java | 18 +- .../security/support/ConfigConstants.java | 175 +- .../security/support/ConfigHelper.java | 64 +- .../GuardedSearchOperationWrapper.java | 1 + .../security/support/HTTPHelper.java | 33 +- .../security/support/HeaderHelper.java | 4 +- .../opensearch/security/support/MapUtils.java | 10 +- .../security/support/ModuleInfo.java | 240 +- .../security/support/ModuleType.java | 187 +- .../security/support/PemKeyReader.java | 196 +- .../security/support/ReflectionHelper.java | 15 +- .../support/ReflectiveAttributeAccessors.java | 2 - .../security/support/SecurityJsonNode.java | 12 +- .../security/support/SecuritySettings.java | 17 +- .../security/support/SecurityUtils.java | 80 +- .../support/SnapshotRestoreHelper.java | 8 +- .../security/support/SourceFieldsContext.java | 42 +- .../security/support/WildcardMatcher.java | 30 +- .../security/tools/AuditConfigMigrater.java | 54 +- .../org/opensearch/security/tools/Hasher.java | 20 +- .../opensearch/security/tools/Migrater.java | 113 +- .../security/tools/SecurityAdmin.java | 1547 +++--- .../DefaultInterClusterRequestEvaluator.java | 35 +- .../InterClusterRequestEvaluator.java | 8 +- .../transport/OIDClusterRequestEvaluator.java | 8 +- .../transport/SecurityInterceptor.java | 148 +- .../transport/SecurityRequestHandler.java | 254 +- .../security/user/AuthCredentials.java | 39 +- .../org/opensearch/security/user/User.java | 51 +- .../opensearch/security/user/UserService.java | 67 +- .../ratetracking/HeapBasedRateTracker.java | 37 +- .../ratetracking/SingleTryRateTracker.java | 3 +- .../resources/static_config/static_roles.yml | 150 +- .../http/saml/HTTPSamlAuthenticatorTest.java | 14 +- .../security/dlic/dlsfls/FlsFlatTests.java | 246 + .../dlic/rest/api/RoleBasedAccessTest.java | 12 +- .../PrivilegesEvaluatorUnitTest.java | 36 + .../security/test/helper/rest/RestHelper.java | 41 +- src/test/resources/cache/roles.yml | 656 +-- src/test/resources/dlsfls/roles_fls_flat.yml | 39 + src/test/resources/multitenancy/roles.yml | 702 +-- src/test/resources/restapi/roles.yml | 428 +- src/test/resources/roles.yml | 1386 ++--- src/test/resources/roles_bs.yml | 64 +- src/test/resources/roles_bulk.yml | 22 +- src/test/resources/roles_ccs.yml | 20 +- src/test/resources/security_passive/roles.yml | 56 +- 282 files changed, 23462 insertions(+), 17558 deletions(-) create mode 100644 ARCHITECTURE.md create mode 100644 check-permissions-order.js create mode 100644 src/test/java/org/opensearch/security/dlic/dlsfls/FlsFlatTests.java create mode 100644 src/test/java/org/opensearch/security/privileges/PrivilegesEvaluatorUnitTest.java create mode 100644 src/test/resources/dlsfls/roles_fls_flat.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c6465c5d4e..0a6cd5b141 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,13 +6,34 @@ env: GRADLE_OPTS: -Dhttp.keepAlive=false jobs: - build: - name: build + generate-test-list: + runs-on: ubuntu-latest + outputs: + separateTestsNames: ${{ steps.set-matrix.outputs.separateTestsNames }} + steps: + - name: Set up JDK for build and test + uses: actions/setup-java@v2 + with: + distribution: temurin # Temurin is a distribution of adoptium + java-version: 17 + + - name: Checkout security + uses: actions/checkout@v2 + + - name: Generate list of tasks + id: set-matrix + run: | + echo "separateTestsNames=$(./gradlew listTasksAsJSON -q --console=plain | tail -n 1)" >> $GITHUB_OUTPUT + + test: + name: test + needs: generate-test-list strategy: fail-fast: false matrix: + gradle_task: ${{ fromJson(needs.generate-test-list.outputs.separateTestsNames) }} + platform: [windows-latest, ubuntu-latest] jdk: [11, 17] - platform: ["ubuntu-latest", "windows-latest"] runs-on: ${{ matrix.platform }} steps: @@ -29,12 +50,8 @@ jobs: uses: gradle/gradle-build-action@v2 with: arguments: | - build test -Dbuild.snapshot=false - -x integrationTest - -x spotlessCheck - -x checkstyleMain - -x checkstyleTest - -x spotbugsMain + ${{ matrix.gradle_task }} -Dbuild.snapshot=false + -x test - name: Coverage uses: codecov/codecov-action@v1 @@ -59,7 +76,7 @@ jobs: fail-fast: false matrix: jdk: [17] - platform: ["ubuntu-latest", "windows-latest"] + platform: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: @@ -78,18 +95,13 @@ jobs: with: arguments: | integrationTest -Dbuild.snapshot=false - -x spotlessCheck - -x checkstyleMain - -x checkstyleTest - -x spotbugsMain backward-compatibility: - strategy: fail-fast: false matrix: jdk: [11, 17] - platform: ["ubuntu-latest", "windows-latest"] + platform: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.platform }} steps: diff --git a/.github/workflows/code-hygiene.yml b/.github/workflows/code-hygiene.yml index f078cb2b56..04590fc7fd 100644 --- a/.github/workflows/code-hygiene.yml +++ b/.github/workflows/code-hygiene.yml @@ -57,3 +57,26 @@ jobs: - uses: gradle/gradle-build-action@v2 with: arguments: spotbugsMain + + check-permissions-order: + runs-on: ubuntu-latest + name: Check permissions orders + steps: + - uses: actions/checkout@v2 + - run: npm install yaml + + - name: Check permissions order + run: | + exclude_pattern="(^|/)roles_invalidxcontent.yml($|/) + (^|/)invalid_config/config.yml($|/)" + # Set pattern to exclude certain files + set -e + exit_code=0 + for file in $(find . -name '*.yml' | grep -Ev "$exclude_pattern"); do + if ! node check-permissions-order.js "$file" --slient; then + exit_code=1 + echo "Error: $file requires changes. Run the following command to fix:" + echo "node check-permissions-order.js $file --fix" + fi + done + exit $exit_code diff --git a/.gitignore b/.gitignore index 5eb2da999f..6fbfafabac 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ out/ build/ gradle-build/ .gradle/ + +# nodejs +node_modules/ +package-lock.json diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000000..75a1fc9825 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,131 @@ +# OpenSearch Security Plugin Architecture + +OpenSearch’s core systems do not include security features, these features are added by installing the Security Plugin. The Security Plugin extends OpenSearch to provide authentication, authorization, end to end Encryption, audit logging, and management interfaces. + +## Components + +The Security Plugin is packaged into a standard plugin zip file used by OpenSearch which can be installed by using the plugin tool. The security configuration is accessible on disk for modification before the node has been turned on. After node startup, the admin tools or API endpoints can be used for dynamic changes. + +```mermaid +graph TD + subgraph OpenSearch Node + subgraph File System + cfg[Security Configuration files] + adm[Admin Tools] + end + subgraph Indices + idx(Index 1..n) + secIdx[Security Index] + end + subgraph Plugins + pgns(Plugins 1..n) + sec[Security Plugin] + end + + sec -- bootstrap security config --> cfg + sec -- refresh security config from cluster --> secIdx + adm -- backup/restore security config --> sec + end +``` + +### Security Plugin + +The runtime of the Security Plugin uses extension points to insert itself into the path actions. Several security management actions are registered in OpenSearch so they can be changed through REST API actions. + +### Security Configuration + +The security configuration is stored in an system index that is replicated to all nodes. When a change has been made to the configuration, the Security Plugin is reloaded to cleanly initialize its components with the new settings. + +#### Configuration Files + +When starting up with no security index detected in the cluster, the Security Plugin will attempt to load configuration files from disk into a new security index. The configuration files can be manually modified or sourced from a backup of a security index created using the admin tools. + +### Admin Tools + +For OpenSearch nodes to join a cluster, they need to have the same security configuration. Complete security configurations will include SSL settings and certificate files. The admin tools allow users to manage these settings and other features. + +## Flows + +### Authentication / Authorization + +The Security Plugin supports multiple authentication backends including an internal identity provider which works with HTTP basic authentication as well as support [external providers](https://opensearch.org/docs/latest/security/authentication-backends/authc-index/) such as OpenId Connect (OIDC) and SAML. + +Authorization is governed by roles declared in the security configuration. Roles control resource access by referencing the transport action name and/or index names in combination with OpenSearch action names. + +Users are assigned roles via the role mappings. These mappings include backend role assignments from authentication providers as well as internal roles defined in the Security Plugin. + +```mermaid +sequenceDiagram + title Basic Authorization flow + autonumber + participant C as Client + participant O as OpenSearch + participant SP as Security Plugin + participant RH as Request Handler + participant AL as Audit Log + + C->>O: Request + O->>SP: Request Received + activate SP + SP->>SP: Authenticate user via internal/external auth providers + SP->>SP: Resolve Authorization for user + SP-->>O: Allow/Deny request + SP->>AL: Update Audit Log asynchronously + deactivate SP + O->>RH: Request continues to request handler + RH-->>O: Result + O->>C: Response +``` + +#### Multiple Authorization Provider flow + +Based on the order within the Security Plugin's configuration authentication providers are iterated through to discover which provider can authenticate the user. + +```mermaid +sequenceDiagram + title Multiple Authorization Provider flow + autonumber + participant C as Client + participant SP as Security Plugin + participant IAP as Internal Auth Provider + participant EAP as External Auth Provider* + participant SC as Security Configuration + + C->>SP: Incoming request + SP->>IAP: Attempt to authenticate internally + IAP-->>SP: Internal user result + loop for each External Auth Provider + SP->>EAP: Attempt to authenticate + EAP-->>SP: External user result + end + SP->>SC: Check Authorization rules + SC->>SC: Match user roles & permissions + SC-->>SP: Authorization result + SP-->>C: Response +``` + +#### Rest vs Transport flow + +OpenSearch treats external REST requests differently than internal transport requests. While REST requests allow for client-to-node communication and make use of API routes, transport requests are more structured and are used to communicate between nodes. + +```mermaid +sequenceDiagram + title Rest vs Transport Flow + autonumber + participant C as Client + participant O as OpenSearch + participant SP as Security Plugin (Rest Filter & Security Interceptor) + participant AH as Action Handler + + C->>O: Request + O->>SP: REST Request Received + SP->>SP: If using client cert, Authenticate + SP-->>O: Continue request + O->>SP: Transport Request Received + SP->>SP: Authenticate user via internal/external auth providers + SP->>SP: Resolve Authorization for user + SP-->>O: Allow/Deny request + O->>AH: Send transport request to action handler + AH-->>O: Result + O->>C: Response +``` diff --git a/TRIAGING.md b/TRIAGING.md index bb61779a7c..a67e1f5f1f 100644 --- a/TRIAGING.md +++ b/TRIAGING.md @@ -25,9 +25,8 @@ Meetings are lightly structured as follows: 1. Announcements: If there are any announcements to be made they will happen at the start of the meeting. 2. Review of new issues: The meetings always start with reviewing all untriaged [issues](https://github.com/search?q=label%3Auntriaged+is%3Aopen++repo%3Aopensearch-project%2Fsecurity+repo%3Aopensearch-project%2Fsecurity-dashboards-plugin&type=issues&ref=advsearch&s=created&o=desc) for the security and security-dashboards repositories. 3. Untriaged items: Review any [issues](https://github.com/search?q=-label%3Atriaged+is%3Aopen++is%3Aissue+repo%3Aopensearch-project%2Fsecurity+repo%3Aopensearch-project%2Fsecurity-dashboards-plugin&type=issues) that might have had the 'untriaged' label removed but require additional triage discussion. -4. Open discussion: Next, we open the floor in case anyone wants to highlight an issue. -5. Backlog discussion: Then, we review issues from the [backlogs](https://github.com/search?q=label%3A%22sprint+backlog%22+is%3Aopen++repo%3Aopensearch-project%2Fsecurity+repo%3Aopensearch-project%2Fsecurity-dashboards-plugin&type=issues&ref=advsearch&s=created&o=desc) of the security and security-dashboards repositories. -6. Least recent discussed issue: Finally, to close out the meeting we will [review the oldest](https://github.com/search?q=+is%3Aopen++repo%3Aopensearch-project%2Fsecurity+repo%3Aopensearch-project%2Fsecurity-dashboards-plugin&type=issues&ref=advsearch&s=updated&o=asc) issues from both repositories, security and security-dashboards, to help identify issues that have languished. +4. Pull request discussion: Then, we review the status of outstanding [pull requests](https://github.com/search?q=+is%3Aopen++repo%3Aopensearch-project%2Fsecurity+repo%3Aopensearch-project%2Fsecurity-dashboards-plugin&type=pullrequests&ref=advsearch) from the security and security-dashboards repositories. +5. Open discussion: Finally, we open the floor in case anyone wants to highlight an issue. There is no specific ordering within each category. diff --git a/build.gradle b/build.gradle index 15eaad0803..c1302656c4 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ import com.diffplug.gradle.spotless.JavaExtension import org.opensearch.gradle.test.RestIntegTestTask +import groovy.json.JsonBuilder buildscript { ext { @@ -24,7 +25,7 @@ buildscript { opensearch_build = version_tokens[0] + '.0' common_utils_version = System.getProperty("common_utils.version", '3.0.0.0-SNAPSHOT') - kafka_version = '3.4.0' + kafka_version = '3.5.0' if (buildVersionQualifier) { opensearch_build += "-${buildVersionQualifier}" @@ -70,54 +71,7 @@ apply plugin: 'opensearch.opensearchplugin' apply plugin: 'opensearch.pluginzip' apply plugin: 'opensearch.rest-test' apply plugin: 'opensearch.testclusters' -// apply from: 'gradle/formatting.gradle' - -spotless { - java { - // Normally this isn't necessary, but we have Java sources in - // non-standard places - target '**/com/amazon/dlic/**/*.java' - target '**/com/amazon/security/**/*.java' - target '**/test/**/*.java' - - removeUnusedImports() - eclipse().configFile rootProject.file('formatter/formatterConfig.xml') - trimTrailingWhitespace() - endWithNewline(); - - // note: you can use an empty string for all the imports you didn't specify explicitly, and '\\#` prefix for static imports - importOrder('java', 'javax', '', 'com.amazon', 'org.opensearch', '\\#') - - custom 'Refuse wildcard imports', { - // Wildcard imports can't be resolved; fail the build - if (it =~ /\s+import .*\*;/) { - throw new AssertionError("Do not use wildcard imports. 'spotlessApply' cannot resolve this issue.") - } - } - - // See DEVELOPER_GUIDE.md for details of when to enable this. - if (System.getProperty('spotless.paddedcell') != null) { - paddedCell() - } - } - format 'misc', { - target '*.md', '*.gradle', '**/*.json', '**/*.yaml', '**/*.yml', '**/*.svg' - - trimTrailingWhitespace() - endWithNewline() - } - format('javaFoo', JavaExtension) { - - importOrder('java', 'javax', '', 'com.amazon', 'org.opensearch', '\\#') - target '**/*.java' - targetExclude '**/com/amazon/dlic/**/*.java' - targetExclude '**/com/amazon/security/**/*.java' - targetExclude '**/test/**/*.java' - - trimTrailingWhitespace() - endWithNewline(); - } -} +apply from: 'gradle/formatting.gradle' licenseFile = rootProject.file('LICENSE.txt') noticeFile = rootProject.file('NOTICE.txt') @@ -152,12 +106,85 @@ tasks.whenTaskAdded {task -> } } +def splitTestConfig = [ + ciSecurityIntegrationTest: [ + description: "Runs integration tests from all classes.", + filters: [ + includeTestsMatching: [ + "org.opensearch.security.*Integ*" + ], + excludeTestsMatching: [ + "org.opensearch.security.sanity.tests.*" + ] + ] + ], + crossClusterTest: [ + description: "Runs cross-cluster tests.", + filters: [ + includeTestsMatching: [ + "org.opensearch.security.ccstest.*" + ] + ] + ], + dlicDlsflsTest: [ + description: "Runs Document- and Field-Level Security tests.", + filters: [ + includeTestsMatching: [ + "org.opensearch.security.dlic.dlsfls.*" + ] + ] + ], + dlicRestApiTest: [ + description: "Runs REST Management API tests.", + filters: [ + includeTestsMatching: [ + "org.opensearch.security.dlic.rest.*" + ] + ] + ], + indicesTest: [ + description: "Runs indices tests from all classes.", + filters: [ + includeTestsMatching: [ + "org.opensearch.security.*indices*" + ], + excludeTestsMatching: [ + "org.opensearch.security.sanity.tests.*" + ] + ] + ], + opensslCITest: [ + description: "Runs portion of SSL tests related to OpenSSL. Explained in https://github.com/opensearch-project/security/pull/2301", + include: '**/OpenSSL*.class' + ], + sslTest: [ + description: "Runs most of the SSL tests.", + filters: [ + includeTestsMatching: [ + "org.opensearch.security.ssl.*" + ], + excludeTestsMatching: [ + "org.opensearch.security.ssl.OpenSSL*" + ] + ] + ] +] as ConfigObject + +List taskNames = splitTestConfig.keySet() as List + +task listTasksAsJSON { + // We are using `doLast` to explicitly specify when we + // want this action to be started. Without it the output + // is not shown at all or can be mixed with other outputs. + doLast { + System.out.println(new JsonBuilder(["citest"] + taskNames)) + } +} test { include '**/*.class' filter { excludeTestsMatching "org.opensearch.security.sanity.tests.*" - excludeTestsMatching "org.opensearch.security.ssl.OpenSSL*" } maxParallelForks = 8 jvmArgs += "-Xmx3072m" @@ -185,14 +212,37 @@ test { } } -//add new task that runs OpenSSL tests -task opensslTest(type: Test) { - include '**/OpenSSL*.class' - retry { +task copyExtraTestResources(dependsOn: testClasses) { + + copy { + from 'src/test/resources' + into 'build/testrun/test/src/test/resources' + } + + taskNames.each { testName -> + copy { + from 'src/test/resources' + into "build/testrun/${testName}/src/test/resources" + } + } + + copy { + from 'src/test/resources' + into 'build/testrun/citest/src/test/resources' + } +} + +def setCommonTestConfig(Test task) { + task.maxParallelForks = 8 + task.jvmArgs += "-Xmx3072m" + if (JavaVersion.current() > JavaVersion.VERSION_1_8) { + task.jvmArgs += "--add-opens=java.base/java.io=ALL-UNNAMED" + } + task.retry { failOnPassedAfterRetry = false maxRetries = 5 } - jacoco { + task.jacoco { excludes = [ "com.sun.jndi.dns.*", "com.sun.security.sasl.gsskerb.*", @@ -207,21 +257,58 @@ task opensslTest(type: Test) { "sun.util.resources.provider.*" ] } + task.dependsOn copyExtraTestResources + task.finalizedBy jacocoTestReport } -task copyExtraTestResources(dependsOn: testClasses) { - copy { - from 'src/test/resources' - into 'build/testrun/test/src/test/resources' +task citest(type: Test) { + group = "Github Actions tests" + description = "Runs the test suite on classes not covered by rest of the task in this group." + include '**/*.class' + filter { + excludeTestsMatching "org.opensearch.security.sanity.tests.*" + excludeTestsMatching "org.opensearch.security.ssl.OpenSSL*" + splitTestConfig.each { entry -> + entry.value.filters.each{ test -> + if (test.key == "includeTestsMatching") { + test.value.each{ + excludeTestsMatching "${it}" + } + } else if (test.key == "includeTest") { + test.value.each{ + excludeTest "${it}" + } + } + } + } } + setCommonTestConfig(it) } -tasks.test.dependsOn(copyExtraTestResources, opensslTest) + +splitTestConfig.each{ testName, testCfg -> + task "${testName}"(type: Test) { + group = testCfg.group ?: "Github Actions tests" + description = testCfg.description + include testCfg.include ?: '**/*.class' + filter { + testCfg.filters.each{ filter, values -> + values.each{ value -> + "${filter}" "${value}" + } + } + } + setCommonTestConfig(it) + } +} + +tasks.test.dependsOn(copyExtraTestResources) jacoco { reportsDirectory = file("$buildDir/reports/jacoco") } jacocoTestReport { + getExecutionData().setFrom(fileTree(buildDir).include("/jacoco/*.exec")) reports { xml.required = true } @@ -337,6 +424,7 @@ configurations { force "io.netty:netty-transport-native-unix-common:${versions.netty}" force "org.apache.bcel:bcel:6.6.0" // This line should be removed once Spotbugs is upgraded to 4.7.4 force "com.github.luben:zstd-jni:${versions.zstd}" + force "org.xerial.snappy:snappy-java:1.1.10.1" } } @@ -388,10 +476,10 @@ dependencies { implementation "org.apache.httpcomponents:httpclient:${versions.httpclient}" implementation "org.apache.httpcomponents:httpcore:${versions.httpcore}" implementation "org.apache.httpcomponents:httpasyncclient:${versions.httpasyncclient}" - implementation 'com.google.guava:guava:30.0-jre' + implementation 'com.google.guava:guava:32.0.1-jre' implementation 'org.greenrobot:eventbus:3.2.0' implementation 'commons-cli:commons-cli:1.3.1' - implementation "org.bouncycastle:bcprov-jdk15on:${versions.bouncycastle}" + implementation "org.bouncycastle:bcprov-jdk15to18:${versions.bouncycastle}" implementation 'org.ldaptive:ldaptive:1.2.3' implementation 'io.jsonwebtoken:jjwt-api:0.10.8' implementation('org.apache.cxf:cxf-rt-rs-security-jose:3.5.5') { @@ -453,7 +541,7 @@ dependencies { runtimeOnly 'io.dropwizard.metrics:metrics-core:3.1.2' runtimeOnly 'org.slf4j:slf4j-api:1.7.30' runtimeOnly 'org.apache.logging.log4j:log4j-slf4j-impl:2.17.1' - runtimeOnly 'org.xerial.snappy:snappy-java:1.1.8.4' + runtimeOnly 'org.xerial.snappy:snappy-java:1.1.10.1' runtimeOnly 'org.codehaus.woodstox:stax2-api:4.2.1' runtimeOnly 'org.glassfish.jaxb:txw2:2.3.4' runtimeOnly 'com.fasterxml.woodstox:woodstox-core:6.4.0' @@ -461,7 +549,8 @@ dependencies { runtimeOnly 'org.apache.santuario:xmlsec:2.2.3' runtimeOnly "com.github.luben:zstd-jni:${versions.zstd}" runtimeOnly 'org.checkerframework:checker-qual:3.5.0' - runtimeOnly "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}" + runtimeOnly "org.bouncycastle:bcpkix-jdk15to18:${versions.bouncycastle}" + runtimeOnly 'org.scala-lang.modules:scala-java8-compat_3:1.0.2' implementation 'org.apache.commons:commons-lang3:3.4' @@ -524,8 +613,8 @@ dependencies { integrationTestImplementation 'org.apache.logging.log4j:log4j-core:2.17.1' integrationTestImplementation 'org.apache.logging.log4j:log4j-jul:2.17.1' integrationTestImplementation 'org.hamcrest:hamcrest:2.2' - integrationTestImplementation "org.bouncycastle:bcpkix-jdk15on:${versions.bouncycastle}" - integrationTestImplementation "org.bouncycastle:bcutil-jdk15on:${versions.bouncycastle}" + integrationTestImplementation "org.bouncycastle:bcpkix-jdk15to18:${versions.bouncycastle}" + integrationTestImplementation "org.bouncycastle:bcutil-jdk15to18:${versions.bouncycastle}" integrationTestImplementation('org.awaitility:awaitility:4.2.0') { exclude(group: 'org.hamcrest', module: 'hamcrest') } diff --git a/check-permissions-order.js b/check-permissions-order.js new file mode 100644 index 0000000000..934694fc73 --- /dev/null +++ b/check-permissions-order.js @@ -0,0 +1,86 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + * + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +const fs = require('fs') +const yaml = require('yaml') + +function checkPermissionsOrder(file, fix = false) { + const contents = fs.readFileSync(file, 'utf8') + const doc = yaml.parseDocument(contents, { keepCstNodes: true }) + const roles = doc.contents.items + let requiresChanges = false + roles.forEach(role => { + const itemsFromRole = role?.value?.items; + + const clusterPermissions = itemsFromRole?.filter(item => item.key && item.key.value === 'cluster_permissions'); + requiresChanges |= checkPermissionsOrdering(clusterPermissions); + + + const indexPermissionsArray = itemsFromRole?.filter(item => item.key && item.key.value === 'index_permissions'); + const indexPermissionObj = indexPermissionsArray?.[0]?.value; + const indexPermissionItems = indexPermissionObj?.items[0]?.items; + const allowedIndexActions = indexPermissionItems?.filter(item => item.key && item.key.value === 'allowed_actions'); + + requiresChanges |= checkPermissionsOrdering(allowedIndexActions); + }) + + if (fix && requiresChanges) { + const newContents = doc.toString() + fs.writeFileSync(file, newContents, 'utf8') + } + + return requiresChanges +} + +/* + Checks the permissions ordering + + returns false if they are already stored + returns true if the permissions were not sored, note the permissions object are sorted as a side effect of this function +*/ +function checkPermissionsOrdering(permissions) { + let requiresChanges = false; + if (!permissions) { + return requiresChanges; + } + permissions.forEach(permission => { + const items = permission.value.items; + const originalItems = JSON.stringify(items); + items.sort(); + const sortedItems = JSON.stringify(items); + + // If the original items and sorted items are not the same, then changes are required + if (originalItems !== sortedItems) { + requiresChanges = true; + } + }); + return requiresChanges; +} + +// Example usage +const args = process.argv.slice(2) +if (args.length === 0) { + console.error('Usage: node check-permissions-order.js [--fix] [--silent]') + process.exit(1) +} +const filePath = args[0] +const fix = args.includes('--fix') +const slient = args.includes('--slient') +if (checkPermissionsOrder(filePath, fix)) { + if (fix) { + if (!slient) { console.log(`${filePath} has been updated.`) } + } else { + if (!slient) { console.error(`Error: ${filePath} requires changes.`) } + process.exit(1) + } +} else { + if (!slient) { console.log(`${filePath} is up-to-date.`) } +} diff --git a/config/roles.yml b/config/roles.yml index d03b47ab28..05bdb6569b 100644 --- a/config/roles.yml +++ b/config/roles.yml @@ -43,17 +43,17 @@ alerting_ack_alerts: alerting_full_access: reserved: true cluster_permissions: - - 'cluster_monitor' - 'cluster:admin/opendistro/alerting/*' - 'cluster:admin/opensearch/alerting/*' - 'cluster:admin/opensearch/notifications/feature/publish' + - 'cluster_monitor' index_permissions: - index_patterns: - '*' allowed_actions: - - 'indices_monitor' - 'indices:admin/aliases/get' - 'indices:admin/mappings/get' + - 'indices_monitor' # Allow users to read Anomaly Detection detectors and results anomaly_read_access: @@ -61,65 +61,65 @@ anomaly_read_access: cluster_permissions: - 'cluster:admin/opendistro/ad/detector/info' - 'cluster:admin/opendistro/ad/detector/search' + - 'cluster:admin/opendistro/ad/detector/validate' - 'cluster:admin/opendistro/ad/detectors/get' - 'cluster:admin/opendistro/ad/result/search' - - 'cluster:admin/opendistro/ad/tasks/search' - - 'cluster:admin/opendistro/ad/detector/validate' - 'cluster:admin/opendistro/ad/result/topAnomalies' + - 'cluster:admin/opendistro/ad/tasks/search' # Allows users to use all Anomaly Detection functionality anomaly_full_access: reserved: true cluster_permissions: - - 'cluster_monitor' - 'cluster:admin/opendistro/ad/*' + - 'cluster_monitor' index_permissions: - index_patterns: - '*' allowed_actions: - - 'indices_monitor' - 'indices:admin/aliases/get' - 'indices:admin/mappings/get' + - 'indices_monitor' # Allow users to execute read only k-NN actions knn_read_access: reserved: true cluster_permissions: - - 'cluster:admin/knn_search_model_action' - 'cluster:admin/knn_get_model_action' + - 'cluster:admin/knn_search_model_action' - 'cluster:admin/knn_stats_action' # Allow users to use all k-NN functionality knn_full_access: reserved: true cluster_permissions: - - 'cluster:admin/knn_training_model_action' - - 'cluster:admin/knn_training_job_router_action' - - 'cluster:admin/knn_training_job_route_decision_info_action' - - 'cluster:admin/knn_warmup_action' - 'cluster:admin/knn_delete_model_action' + - 'cluster:admin/knn_get_model_action' - 'cluster:admin/knn_remove_model_from_cache_action' - - 'cluster:admin/knn_update_model_graveyard_action' - 'cluster:admin/knn_search_model_action' - - 'cluster:admin/knn_get_model_action' - 'cluster:admin/knn_stats_action' + - 'cluster:admin/knn_training_job_route_decision_info_action' + - 'cluster:admin/knn_training_job_router_action' + - 'cluster:admin/knn_training_model_action' + - 'cluster:admin/knn_update_model_graveyard_action' + - 'cluster:admin/knn_warmup_action' # Allows users to read Notebooks notebooks_read_access: reserved: true cluster_permissions: - - 'cluster:admin/opendistro/notebooks/list' - 'cluster:admin/opendistro/notebooks/get' + - 'cluster:admin/opendistro/notebooks/list' # Allows users to all Notebooks functionality notebooks_full_access: reserved: true cluster_permissions: - 'cluster:admin/opendistro/notebooks/create' - - 'cluster:admin/opendistro/notebooks/update' - 'cluster:admin/opendistro/notebooks/delete' - 'cluster:admin/opendistro/notebooks/get' - 'cluster:admin/opendistro/notebooks/list' + - 'cluster:admin/opendistro/notebooks/update' # Allows users to read observability objects observability_read_access: @@ -132,9 +132,9 @@ observability_full_access: reserved: true cluster_permissions: - 'cluster:admin/opensearch/observability/create' - - 'cluster:admin/opensearch/observability/update' - 'cluster:admin/opensearch/observability/delete' - 'cluster:admin/opensearch/observability/get' + - 'cluster:admin/opensearch/observability/update' # Allows users to all PPL functionality ppl_full_access: @@ -153,8 +153,8 @@ ppl_full_access: reports_instances_read_access: reserved: true cluster_permissions: - - 'cluster:admin/opendistro/reports/instance/list' - 'cluster:admin/opendistro/reports/instance/get' + - 'cluster:admin/opendistro/reports/instance/list' - 'cluster:admin/opendistro/reports/menu/download' # Allows users to read and download Reports and Report-definitions @@ -163,8 +163,8 @@ reports_read_access: cluster_permissions: - 'cluster:admin/opendistro/reports/definition/get' - 'cluster:admin/opendistro/reports/definition/list' - - 'cluster:admin/opendistro/reports/instance/list' - 'cluster:admin/opendistro/reports/instance/get' + - 'cluster:admin/opendistro/reports/instance/list' - 'cluster:admin/opendistro/reports/menu/download' # Allows users to all Reports functionality @@ -172,13 +172,13 @@ reports_full_access: reserved: true cluster_permissions: - 'cluster:admin/opendistro/reports/definition/create' - - 'cluster:admin/opendistro/reports/definition/update' - - 'cluster:admin/opendistro/reports/definition/on_demand' - 'cluster:admin/opendistro/reports/definition/delete' - 'cluster:admin/opendistro/reports/definition/get' - 'cluster:admin/opendistro/reports/definition/list' - - 'cluster:admin/opendistro/reports/instance/list' + - 'cluster:admin/opendistro/reports/definition/on_demand' + - 'cluster:admin/opendistro/reports/definition/update' - 'cluster:admin/opendistro/reports/instance/get' + - 'cluster:admin/opendistro/reports/instance/list' - 'cluster:admin/opendistro/reports/menu/download' # Allows users to use all asynchronous-search functionality @@ -234,14 +234,14 @@ cross_cluster_replication_follower_full_access: - index_patterns: - '*' allowed_actions: - - "indices:admin/plugins/replication/index/setup/validate" - - "indices:data/write/plugins/replication/changes" - - "indices:admin/plugins/replication/index/start" - "indices:admin/plugins/replication/index/pause" - "indices:admin/plugins/replication/index/resume" + - "indices:admin/plugins/replication/index/setup/validate" + - "indices:admin/plugins/replication/index/start" + - "indices:admin/plugins/replication/index/status_check" - "indices:admin/plugins/replication/index/stop" - "indices:admin/plugins/replication/index/update" - - "indices:admin/plugins/replication/index/status_check" + - "indices:data/write/plugins/replication/changes" # Allows users to use all cross cluster search functionality at remote cluster cross_cluster_search_remote_full_access: @@ -257,7 +257,7 @@ cross_cluster_search_remote_full_access: ml_read_access: reserved: true cluster_permissions: - - 'cluster:admin/opensearch/ml/stats/nodes' + - 'cluster:admin/opensearch/ml/model_groups/search' - 'cluster:admin/opensearch/ml/models/get' - 'cluster:admin/opensearch/ml/models/search' - 'cluster:admin/opensearch/ml/tasks/get' @@ -267,8 +267,8 @@ ml_read_access: ml_full_access: reserved: true cluster_permissions: - - 'cluster_monitor' - 'cluster:admin/opensearch/ml/*' + - 'cluster_monitor' index_permissions: - index_patterns: - '*' @@ -285,16 +285,16 @@ notifications_full_access: notifications_read_access: reserved: true cluster_permissions: + - 'cluster:admin/opensearch/notifications/channels/get' - 'cluster:admin/opensearch/notifications/configs/get' - 'cluster:admin/opensearch/notifications/features' - - 'cluster:admin/opensearch/notifications/channels/get' # Allows users to use all snapshot management functionality snapshot_management_full_access: reserved: true cluster_permissions: - - 'cluster:admin/opensearch/snapshot_management/*' - 'cluster:admin/opensearch/notifications/feature/publish' + - 'cluster:admin/opensearch/snapshot_management/*' - 'cluster:admin/repository/*' - 'cluster:admin/snapshot/*' @@ -302,9 +302,9 @@ snapshot_management_full_access: snapshot_management_read_access: reserved: true cluster_permissions: + - 'cluster:admin/opensearch/snapshot_management/policy/explain' - 'cluster:admin/opensearch/snapshot_management/policy/get' - 'cluster:admin/opensearch/snapshot_management/policy/search' - - 'cluster:admin/opensearch/snapshot_management/policy/explain' - 'cluster:admin/repository/get' - 'cluster:admin/snapshot/get' diff --git a/gradle/formatting.gradle b/gradle/formatting.gradle index 40ae51afb1..de52b51c83 100644 --- a/gradle/formatting.gradle +++ b/gradle/formatting.gradle @@ -1,95 +1,26 @@ -/* -* SPDX-License-Identifier: Apache-2.0 -* -* The OpenSearch Contributors require contributions made to -* this file be licensed under the Apache-2.0 license or a -* compatible open source license. -* -* Modifications Copyright OpenSearch Contributors. See -* GitHub history for details. -*/ - -/* - * Licensed to Elasticsearch under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch 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. - */ - -import org.opensearch.gradle.BuildPlugin - -/* - * This script plugin configures formatting for Java source using Spotless - * for Gradle. Since the act of formatting existing source can interfere - * with developers' workflows, we don't automatically format all code - * (yet). Instead, we maintain a list of projects that are excluded from - * formatting, until we reach a point where we can comfortably format them - * in one go without too much disruption. - * - * Any new sub-projects must not be added to the exclusions list! - * - * To perform a reformat, run: - * - * ./gradlew spotlessApply - * - * To check the current format, run: - * - * ./gradlew spotlessJavaCheck - * - * This is also carried out by the `precommit` task. - * - * For more about Spotless, see: - * - * https://github.com/diffplug/spotless/tree/master/plugin-gradle - */ - -org.opensearch.gradle.BuildPlugin { - plugins.withType(BuildPlugin).whenPluginAdded { - project.apply plugin: "com.diffplug.spotless" - - spotless { - java { - // Normally this isn't necessary, but we have Java sources in - // non-standard places - target '**/*.java' - - removeUnusedImports() - eclipse().configFile rootProject.file('buildSrc/formatterConfig.xml') - trimTrailingWhitespace() - endWithNewline() - - custom 'Refuse wildcard imports', { - // Wildcard imports can't be resolved; fail the build - if (it =~ /\s+import .*\*;/) { - throw new AssertionError("Do not use wildcard imports. 'spotlessApply' cannot resolve this issue.") - } - } - - // See DEVELOPER_GUIDE.md for details of when to enable this. - if (System.getProperty('spotless.paddedcell') != null) { - paddedCell() - } - } - format 'misc', { - target '*.md', '*.gradle', '**/*.yaml', '**/*.yml', '**/*.svg' - - trimTrailingWhitespace() - endWithNewline() +allprojects { + project.apply plugin: "com.diffplug.spotless" + spotless { + java { + // Normally this isn't necessary, but we have Java sources in + // non-standard places + target '**/*.java' + + removeUnusedImports() + eclipse().configFile rootProject.file('formatter/formatterConfig.xml') + trimTrailingWhitespace() + endWithNewline(); + + // See DEVELOPER_GUIDE.md for details of when to enable this. + if (System.getProperty('spotless.paddedcell') != null) { + paddedCell() } } + format 'misc', { + target '*.md', '*.gradle', '**/*.json', '**/*.yaml', '**/*.yml', '**/*.svg' - precommit.dependsOn 'spotlessJavaCheck' + trimTrailingWhitespace() + endWithNewline() + } } } diff --git a/plugin-security.policy b/plugin-security.policy index 17b57c57b1..7bb18f76c9 100644 --- a/plugin-security.policy +++ b/plugin-security.policy @@ -55,10 +55,13 @@ grant { permission java.net.NetPermission "getNetworkInformation"; permission java.net.NetPermission "getProxySelector"; permission java.net.SocketPermission "*", "connect,accept,resolve"; - + + // BouncyCastle permissions permission java.security.SecurityPermission "putProviderProperty.BC"; permission java.security.SecurityPermission "insertProvider.BC"; - + permission java.security.SecurityPermission "removeProviderProperty.BC"; + permission java.util.PropertyPermission "jdk.tls.rejectClientInitiatedRenegotiation", "write"; + permission java.lang.RuntimePermission "accessUserInformation"; permission java.security.SecurityPermission "org.apache.xml.security.register"; diff --git a/src/integrationTest/java/org/opensearch/common/logging/NodeAndClusterIdConverter.java b/src/integrationTest/java/org/opensearch/common/logging/NodeAndClusterIdConverter.java index 94242ecc28..4aba6c976b 100644 --- a/src/integrationTest/java/org/opensearch/common/logging/NodeAndClusterIdConverter.java +++ b/src/integrationTest/java/org/opensearch/common/logging/NodeAndClusterIdConverter.java @@ -21,13 +21,9 @@ class NodeAndClusterIdConverter { + public NodeAndClusterIdConverter() {} - public NodeAndClusterIdConverter() { - } + public static void setNodeIdAndClusterId(String nodeId, String clusterUUID) {} - public static void setNodeIdAndClusterId(String nodeId, String clusterUUID) { - } - - public void format(LogEvent event, StringBuilder toAppendTo) { - } + public void format(LogEvent event, StringBuilder toAppendTo) {} } diff --git a/src/integrationTest/java/org/opensearch/node/PluginAwareNode.java b/src/integrationTest/java/org/opensearch/node/PluginAwareNode.java index 62b0199824..53e44496ca 100644 --- a/src/integrationTest/java/org/opensearch/node/PluginAwareNode.java +++ b/src/integrationTest/java/org/opensearch/node/PluginAwareNode.java @@ -34,16 +34,19 @@ public class PluginAwareNode extends Node { - private final boolean clusterManagerEligible; - - @SafeVarargs - public PluginAwareNode(boolean clusterManagerEligible, final Settings preparedSettings, final Class... plugins) { - super(InternalSettingsPreparer.prepareEnvironment(preparedSettings, Collections.emptyMap(), null, () -> System.getenv("HOSTNAME")), Arrays.asList(plugins), true); - this.clusterManagerEligible = clusterManagerEligible; - } - - - public boolean isClusterManagerEligible() { - return clusterManagerEligible; - } + private final boolean clusterManagerEligible; + + @SafeVarargs + public PluginAwareNode(boolean clusterManagerEligible, final Settings preparedSettings, final Class... plugins) { + super( + InternalSettingsPreparer.prepareEnvironment(preparedSettings, Collections.emptyMap(), null, () -> System.getenv("HOSTNAME")), + Arrays.asList(plugins), + true + ); + this.clusterManagerEligible = clusterManagerEligible; + } + + public boolean isClusterManagerEligible() { + return clusterManagerEligible; + } } diff --git a/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java index 813b0e39de..287bc139b1 100644 --- a/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java +++ b/src/integrationTest/java/org/opensearch/security/ConfigurationFiles.java @@ -20,47 +20,41 @@ class ConfigurationFiles { - public static void createRoleMappingFile(File destination) { - String resource = "roles_mapping.yml"; - copyResourceToFile(resource, destination); - } + public static void createRoleMappingFile(File destination) { + String resource = "roles_mapping.yml"; + copyResourceToFile(resource, destination); + } - public static void createConfigFile(File destination) { - String resource = "config.yml"; - copyResourceToFile(resource, destination); - } + public static Path createConfigurationDirectory() { + try { + Path tempDirectory = Files.createTempDirectory("test-security-config"); + String[] configurationFiles = { + "config.yml", + "action_groups.yml", + "config.yml", + "internal_users.yml", + "roles.yml", + "roles_mapping.yml", + "security_tenants.yml", + "tenants.yml" }; + for (String fileName : configurationFiles) { + Path configFileDestination = tempDirectory.resolve(fileName); + copyResourceToFile(fileName, configFileDestination.toFile()); + } + return tempDirectory.toAbsolutePath(); + } catch (IOException ex) { + throw new RuntimeException("Cannot create directory with security plugin configuration.", ex); + } + } - public static Path createConfigurationDirectory() { - try { - Path tempDirectory = Files.createTempDirectory("test-security-config"); - String[] configurationFiles = { - "config.yml", - "action_groups.yml", - "config.yml", - "internal_users.yml", - "roles.yml", - "roles_mapping.yml", - "security_tenants.yml", - "tenants.yml" - }; - for (String fileName : configurationFiles) { - Path configFileDestination = tempDirectory.resolve(fileName); - copyResourceToFile(fileName, configFileDestination.toFile()); - } - return tempDirectory.toAbsolutePath(); - } catch (IOException ex) { - throw new RuntimeException("Cannot create directory with security plugin configuration.", ex); - } - } - - private static void copyResourceToFile(String resource, File destination) { - try(InputStream input = ConfigurationFiles.class.getClassLoader().getResourceAsStream(resource)) { - Objects.requireNonNull(input, "Cannot find source resource " + resource); - try(OutputStream output = new FileOutputStream(destination)) { - input.transferTo(output); - } - } catch (IOException e) { - throw new RuntimeException("Cannot create file with security plugin configuration", e); - } - } + private static void copyResourceToFile(String resource, File destination) { + try (InputStream input = ConfigurationFiles.class.getClassLoader().getResourceAsStream(resource)) { + Objects.requireNonNull(input, "Cannot find source resource " + resource); + try (OutputStream output = new FileOutputStream(destination)) { + input.transferTo(output); + } + } catch (IOException e) { + throw new RuntimeException("Cannot create file with security plugin configuration", e); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/CrossClusterSearchTests.java b/src/integrationTest/java/org/opensearch/security/CrossClusterSearchTests.java index 6762274133..ee03913c55 100644 --- a/src/integrationTest/java/org/opensearch/security/CrossClusterSearchTests.java +++ b/src/integrationTest/java/org/opensearch/security/CrossClusterSearchTests.java @@ -68,368 +68,388 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class CrossClusterSearchTests { - private static final String SONG_INDEX_NAME = "song_lyrics"; - - private static final String PROHIBITED_SONG_INDEX_NAME = "prohibited_song_lyrics"; - - public static final String REMOTE_CLUSTER_NAME = "ccsRemote"; - public static final String REMOTE_SONG_INDEX = REMOTE_CLUSTER_NAME + ":" + SONG_INDEX_NAME; - - public static final String SONG_ID_1R = "remote-00001"; - public static final String SONG_ID_2L = "local-00002"; - public static final String SONG_ID_3R = "remote-00003"; - public static final String SONG_ID_4L = "local-00004"; - public static final String SONG_ID_5R = "remote-00005"; - public static final String SONG_ID_6R = "remote-00006"; - - private static final Role LIMITED_ROLE = new Role("limited_role") - .indexPermissions("indices:data/read/search", "indices:admin/shards/search_shards") - .on(SONG_INDEX_NAME, "user-${user.name}-${attr.internal.type}"); - - private static final Role DLS_ROLE_ROCK = new Role("dls_role_rock") - .indexPermissions("indices:data/read/search", "indices:data/read/get", "indices:admin/shards/search_shards") - .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_GENRE, GENRE_ROCK)) - .on(SONG_INDEX_NAME); - - private static final Role DLS_ROLE_JAZZ = new Role("dls_role_jazz") - .indexPermissions("indices:data/read/search", "indices:data/read/get", "indices:admin/shards/search_shards") - .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_GENRE, GENRE_JAZZ)) - .on(SONG_INDEX_NAME); - - private static final Role FLS_EXCLUDE_LYRICS_ROLE = new Role("fls_exclude_lyrics_role") - .indexPermissions("indices:data/read/search", "indices:data/read/get", "indices:admin/shards/search_shards") - .fls("~" + FIELD_LYRICS) - .on(SONG_INDEX_NAME); - - private static final Role FLS_INCLUDE_TITLE_ROLE = new Role("fls_include_title_role") - .indexPermissions("indices:data/read/search", "indices:data/read/get", "indices:admin/shards/search_shards") - .fls(FIELD_TITLE) - .on(SONG_INDEX_NAME); - - public static final String TYPE_ATTRIBUTE = "type"; - - private static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS).attr(TYPE_ATTRIBUTE, "administrative"); - private static final User LIMITED_USER = new User("limited_user").attr(TYPE_ATTRIBUTE, "personal"); - - private static final User FLS_INCLUDE_TITLE_USER = new User("fls_include_title_user"); - - private static final User FLS_EXCLUDE_LYRICS_USER = new User("fls_exclude_lyrics_user"); - - private static final User DLS_USER_ROCK = new User("dls-user-rock"); - - private static final User DLS_USER_JAZZ = new User("dls-user-jazz"); - - public static final String LIMITED_USER_INDEX_NAME = "user-" + LIMITED_USER.getName() + "-" + LIMITED_USER.getAttribute(TYPE_ATTRIBUTE); - public static final String ADMIN_USER_INDEX_NAME = "user-" + ADMIN_USER.getName() + "-" + ADMIN_USER.getAttribute(TYPE_ATTRIBUTE); - - private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); - - private final boolean ccsMinimizeRoundtrips; - - public static final String PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED = "plugins.security.restapi.roles_enabled"; - @ClassRule - public static final LocalCluster remoteCluster = new LocalCluster.Builder().certificates(TEST_CERTIFICATES) - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false).clusterName(REMOTE_CLUSTER_NAME) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .roles(LIMITED_ROLE, DLS_ROLE_ROCK, DLS_ROLE_JAZZ, FLS_EXCLUDE_LYRICS_ROLE, FLS_INCLUDE_TITLE_ROLE).users(ADMIN_USER) - .build(); - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder().certificates(TEST_CERTIFICATES) - .clusterManager(ClusterManager.SINGLE_REMOTE_CLIENT).anonymousAuth(false).clusterName("ccsLocal") - .nodeSettings(Map.of(PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) - .remote(REMOTE_CLUSTER_NAME, remoteCluster) - .roles(LIMITED_ROLE, DLS_ROLE_ROCK, DLS_ROLE_JAZZ, FLS_EXCLUDE_LYRICS_ROLE, FLS_INCLUDE_TITLE_ROLE).authc(AUTHC_HTTPBASIC_INTERNAL) - .users(ADMIN_USER, LIMITED_USER, DLS_USER_ROCK, DLS_USER_JAZZ, FLS_INCLUDE_TITLE_USER, FLS_EXCLUDE_LYRICS_USER) - .build(); - - @ParametersFactory(shuffle = false) - public static Iterable parameters() { - return List.of(new Object[] { true }, new Object[] { false }); - } - - public CrossClusterSearchTests(Boolean ccsMinimizeRoundtrips) { - this.ccsMinimizeRoundtrips = ccsMinimizeRoundtrips; - } - - @BeforeClass - public static void createTestData() { - try(Client client = remoteCluster.getInternalNodeClient()){ - client.prepareIndex(SONG_INDEX_NAME).setId(SONG_ID_1R).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); - client.prepareIndex(SONG_INDEX_NAME).setId(SONG_ID_6R).setRefreshPolicy(IMMEDIATE).setSource(SONGS[5].asMap()).get(); - client.prepareIndex(PROHIBITED_SONG_INDEX_NAME).setId(SONG_ID_3R).setRefreshPolicy(IMMEDIATE).setSource(SONGS[1].asMap()).get(); - client.prepareIndex(LIMITED_USER_INDEX_NAME).setId(SONG_ID_5R).setRefreshPolicy(IMMEDIATE).setSource(SONGS[4].asMap()).get(); - } - try(Client client = cluster.getInternalNodeClient()){ - client.prepareIndex(SONG_INDEX_NAME).setId(SONG_ID_2L).setRefreshPolicy(IMMEDIATE).setSource(SONGS[2].asMap()).get(); - client.prepareIndex(PROHIBITED_SONG_INDEX_NAME).setId(SONG_ID_4L).setRefreshPolicy(IMMEDIATE).setSource(SONGS[3].asMap()).get(); - } - try(TestRestClient client = cluster.getRestClient(ADMIN_USER)) { - client.assignRoleToUser(LIMITED_USER.getName(), LIMITED_ROLE.getName()).assertStatusCode(200); - client.assignRoleToUser(DLS_USER_ROCK.getName(), DLS_ROLE_ROCK.getName()).assertStatusCode(200); - client.assignRoleToUser(DLS_USER_JAZZ.getName(), DLS_ROLE_JAZZ.getName()).assertStatusCode(200); - client.assignRoleToUser(FLS_INCLUDE_TITLE_USER.getName(), FLS_INCLUDE_TITLE_ROLE.getName()).assertStatusCode(200); - client.assignRoleToUser(FLS_EXCLUDE_LYRICS_USER.getName(), FLS_EXCLUDE_LYRICS_ROLE.getName()).assertStatusCode(200); - } - } - - @Test - public void shouldFindDocumentOnRemoteCluster_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = searchAll(REMOTE_SONG_INDEX); - - SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response, numberOfTotalHitsIsEqualTo(2)); - assertThat(response, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, SONG_ID_1R)); - assertThat(response, searchHitsContainDocumentWithId(1, SONG_INDEX_NAME, SONG_ID_6R)); - } - } - - private SearchRequest searchAll(String indexName) { - SearchRequest searchRequest = SearchRequestFactory.searchAll(indexName); - searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); - return searchRequest; - } - - @Test - public void shouldFindDocumentOnRemoteCluster_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":" + PROHIBITED_SONG_INDEX_NAME); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldSearchForDocumentOnRemoteClustersWhenStarIsUsedAsClusterName_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = searchAll("*" + ":" + SONG_INDEX_NAME); - - SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); - - //only remote documents are found - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response, numberOfTotalHitsIsEqualTo(2)); - assertThat(response, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, SONG_ID_1R)); - assertThat(response, searchHitsContainDocumentWithId(1, SONG_INDEX_NAME, SONG_ID_6R)); - } - } - - @Test - public void shouldSearchForDocumentOnRemoteClustersWhenStarIsUsedAsClusterName_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = searchAll("*" + ":" + PROHIBITED_SONG_INDEX_NAME); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldSearchForDocumentOnBothClustersWhenIndexOnBothClusterArePointedOut_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = SearchRequestFactory.searchAll(REMOTE_SONG_INDEX, SONG_INDEX_NAME); - searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); - - SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response, numberOfTotalHitsIsEqualTo(3)); - assertThat(response, searchHitsContainDocumentsInAnyOrder( - Pair.of(SONG_INDEX_NAME, SONG_ID_1R), - Pair.of(SONG_INDEX_NAME, SONG_ID_2L), - Pair.of(SONG_INDEX_NAME, SONG_ID_6R)) - ); - } - } - - @Test - public void shouldSearchForDocumentOnBothClustersWhenIndexOnBothClusterArePointedOut_negativeLackOfLocalAccess() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - var searchRequest = SearchRequestFactory.searchAll(REMOTE_SONG_INDEX, PROHIBITED_SONG_INDEX_NAME); - searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldSearchForDocumentOnBothClustersWhenIndexOnBothClusterArePointedOut_negativeLackOfRemoteAccess() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - String remoteIndex = REMOTE_CLUSTER_NAME + ":" + PROHIBITED_SONG_INDEX_NAME; - SearchRequest searchRequest = SearchRequestFactory.searchAll(remoteIndex, SONG_INDEX_NAME); - searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldSearchViaAllAliasOnRemoteCluster_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { - SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":_all"); - - SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response, numberOfTotalHitsIsEqualTo(4)); - assertThat(response, searchHitsContainDocumentsInAnyOrder( - Pair.of(SONG_INDEX_NAME, SONG_ID_1R), - Pair.of(SONG_INDEX_NAME, SONG_ID_6R), - Pair.of(PROHIBITED_SONG_INDEX_NAME, SONG_ID_3R), - Pair.of(LIMITED_USER_INDEX_NAME, SONG_ID_5R)) - ); - } - } - - @Test - public void shouldSearchViaAllAliasOnRemoteCluster_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":_all"); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldSearchAllIndexOnRemoteClusterWhenStarIsUsedAsIndexName_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { - SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":*"); - - SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response, numberOfTotalHitsIsEqualTo(4)); - assertThat(response, searchHitsContainDocumentsInAnyOrder( - Pair.of(SONG_INDEX_NAME, SONG_ID_1R), - Pair.of(SONG_INDEX_NAME, SONG_ID_6R), - Pair.of(PROHIBITED_SONG_INDEX_NAME, SONG_ID_3R), - Pair.of(LIMITED_USER_INDEX_NAME, SONG_ID_5R)) - ); - } - } - - @Test - public void shouldSearchAllIndexOnRemoteClusterWhenStarIsUsedAsIndexName_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":*"); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldResolveUserNameExpressionInRoleIndexPattern_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":" + LIMITED_USER_INDEX_NAME); - - SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(response, numberOfTotalHitsIsEqualTo(1)); - } - } - - @Test - public void shouldResolveUserNameExpressionInRoleIndexPattern_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":" + ADMIN_USER_INDEX_NAME); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldSearchInIndexWithPrefix_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":song*"); - - SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response, numberOfTotalHitsIsEqualTo(2)); - assertThat(response, searchHitsContainDocumentsInAnyOrder( - Pair.of(SONG_INDEX_NAME, SONG_ID_1R), - Pair.of(SONG_INDEX_NAME, SONG_ID_6R) - )); - } - } - - @Test - public void shouldSearchInIndexWithPrefix_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":prohibited*"); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldEvaluateDocumentLevelSecurityRulesOnRemoteClusterOnSearchRequest_caseRock() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DLS_USER_ROCK)) { - SearchRequest searchRequest = searchAll(REMOTE_SONG_INDEX); - - SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); - - //searching for all documents, so is it important that result contain only one document with id SONG_ID_1 - // and document with SONG_ID_6 is excluded from result set by DLS - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response, numberOfTotalHitsIsEqualTo(1)); - assertThat(response, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, SONG_ID_1R)); - } - } - - @Test - public void shouldEvaluateDocumentLevelSecurityRulesOnRemoteClusterOnSearchRequest_caseJazz() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DLS_USER_JAZZ)) { - SearchRequest searchRequest = searchAll(REMOTE_SONG_INDEX); - - SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); - - //searching for all documents, so is it important that result contain only one document with id SONG_ID_6 - // and document with SONG_ID_1 is excluded from result set by DLS - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response, numberOfTotalHitsIsEqualTo(1)); - assertThat(response, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, SONG_ID_6R)); - } - } - - @Test - public void shouldHaveAccessOnlyToSpecificField() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(FLS_INCLUDE_TITLE_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(REMOTE_SONG_INDEX, QUERY_TITLE_MAGNUM_OPUS); - searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); - - SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response, numberOfTotalHitsIsEqualTo(1)); - //document should contain only title field - assertThat(response, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); - assertThat(response, searchHitDoesNotContainField(0, FIELD_ARTIST)); - assertThat(response, searchHitDoesNotContainField(0, FIELD_LYRICS)); - assertThat(response, searchHitDoesNotContainField(0, FIELD_STARS)); - assertThat(response, searchHitDoesNotContainField(0, FIELD_GENRE)); - } - } - - @Test - public void shouldLackAccessToSpecificField() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(FLS_EXCLUDE_LYRICS_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(REMOTE_SONG_INDEX, QUERY_TITLE_MAGNUM_OPUS); - searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); - - SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response, numberOfTotalHitsIsEqualTo(1)); - //document should not contain lyrics field - assertThat(response, searchHitDoesNotContainField(0, FIELD_LYRICS)); - - assertThat(response, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); - assertThat(response, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_FIRST)); - assertThat(response, searchHitContainsFieldWithValue(0, FIELD_STARS, 1)); - assertThat(response, searchHitContainsFieldWithValue(0, FIELD_GENRE, GENRE_ROCK)); - } - } + private static final String SONG_INDEX_NAME = "song_lyrics"; + + private static final String PROHIBITED_SONG_INDEX_NAME = "prohibited_song_lyrics"; + + public static final String REMOTE_CLUSTER_NAME = "ccsRemote"; + public static final String REMOTE_SONG_INDEX = REMOTE_CLUSTER_NAME + ":" + SONG_INDEX_NAME; + + public static final String SONG_ID_1R = "remote-00001"; + public static final String SONG_ID_2L = "local-00002"; + public static final String SONG_ID_3R = "remote-00003"; + public static final String SONG_ID_4L = "local-00004"; + public static final String SONG_ID_5R = "remote-00005"; + public static final String SONG_ID_6R = "remote-00006"; + + private static final Role LIMITED_ROLE = new Role("limited_role").indexPermissions( + "indices:data/read/search", + "indices:admin/shards/search_shards" + ).on(SONG_INDEX_NAME, "user-${user.name}-${attr.internal.type}"); + + private static final Role DLS_ROLE_ROCK = new Role("dls_role_rock").indexPermissions( + "indices:data/read/search", + "indices:data/read/get", + "indices:admin/shards/search_shards" + ).dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_GENRE, GENRE_ROCK)).on(SONG_INDEX_NAME); + + private static final Role DLS_ROLE_JAZZ = new Role("dls_role_jazz").indexPermissions( + "indices:data/read/search", + "indices:data/read/get", + "indices:admin/shards/search_shards" + ).dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_GENRE, GENRE_JAZZ)).on(SONG_INDEX_NAME); + + private static final Role FLS_EXCLUDE_LYRICS_ROLE = new Role("fls_exclude_lyrics_role").indexPermissions( + "indices:data/read/search", + "indices:data/read/get", + "indices:admin/shards/search_shards" + ).fls("~" + FIELD_LYRICS).on(SONG_INDEX_NAME); + + private static final Role FLS_INCLUDE_TITLE_ROLE = new Role("fls_include_title_role").indexPermissions( + "indices:data/read/search", + "indices:data/read/get", + "indices:admin/shards/search_shards" + ).fls(FIELD_TITLE).on(SONG_INDEX_NAME); + + public static final String TYPE_ATTRIBUTE = "type"; + + private static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS).attr(TYPE_ATTRIBUTE, "administrative"); + private static final User LIMITED_USER = new User("limited_user").attr(TYPE_ATTRIBUTE, "personal"); + + private static final User FLS_INCLUDE_TITLE_USER = new User("fls_include_title_user"); + + private static final User FLS_EXCLUDE_LYRICS_USER = new User("fls_exclude_lyrics_user"); + + private static final User DLS_USER_ROCK = new User("dls-user-rock"); + + private static final User DLS_USER_JAZZ = new User("dls-user-jazz"); + + public static final String LIMITED_USER_INDEX_NAME = "user-" + LIMITED_USER.getName() + "-" + LIMITED_USER.getAttribute(TYPE_ATTRIBUTE); + public static final String ADMIN_USER_INDEX_NAME = "user-" + ADMIN_USER.getName() + "-" + ADMIN_USER.getAttribute(TYPE_ATTRIBUTE); + + private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); + + private final boolean ccsMinimizeRoundtrips; + + public static final String PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED = "plugins.security.restapi.roles_enabled"; + @ClassRule + public static final LocalCluster remoteCluster = new LocalCluster.Builder().certificates(TEST_CERTIFICATES) + .clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .clusterName(REMOTE_CLUSTER_NAME) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .roles(LIMITED_ROLE, DLS_ROLE_ROCK, DLS_ROLE_JAZZ, FLS_EXCLUDE_LYRICS_ROLE, FLS_INCLUDE_TITLE_ROLE) + .users(ADMIN_USER) + .build(); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().certificates(TEST_CERTIFICATES) + .clusterManager(ClusterManager.SINGLE_REMOTE_CLIENT) + .anonymousAuth(false) + .clusterName("ccsLocal") + .nodeSettings(Map.of(PLUGINS_SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName()))) + .remote(REMOTE_CLUSTER_NAME, remoteCluster) + .roles(LIMITED_ROLE, DLS_ROLE_ROCK, DLS_ROLE_JAZZ, FLS_EXCLUDE_LYRICS_ROLE, FLS_INCLUDE_TITLE_ROLE) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER, LIMITED_USER, DLS_USER_ROCK, DLS_USER_JAZZ, FLS_INCLUDE_TITLE_USER, FLS_EXCLUDE_LYRICS_USER) + .build(); + + @ParametersFactory(shuffle = false) + public static Iterable parameters() { + return List.of(new Object[] { true }, new Object[] { false }); + } + + public CrossClusterSearchTests(Boolean ccsMinimizeRoundtrips) { + this.ccsMinimizeRoundtrips = ccsMinimizeRoundtrips; + } + + @BeforeClass + public static void createTestData() { + try (Client client = remoteCluster.getInternalNodeClient()) { + client.prepareIndex(SONG_INDEX_NAME).setId(SONG_ID_1R).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); + client.prepareIndex(SONG_INDEX_NAME).setId(SONG_ID_6R).setRefreshPolicy(IMMEDIATE).setSource(SONGS[5].asMap()).get(); + client.prepareIndex(PROHIBITED_SONG_INDEX_NAME).setId(SONG_ID_3R).setRefreshPolicy(IMMEDIATE).setSource(SONGS[1].asMap()).get(); + client.prepareIndex(LIMITED_USER_INDEX_NAME).setId(SONG_ID_5R).setRefreshPolicy(IMMEDIATE).setSource(SONGS[4].asMap()).get(); + } + try (Client client = cluster.getInternalNodeClient()) { + client.prepareIndex(SONG_INDEX_NAME).setId(SONG_ID_2L).setRefreshPolicy(IMMEDIATE).setSource(SONGS[2].asMap()).get(); + client.prepareIndex(PROHIBITED_SONG_INDEX_NAME).setId(SONG_ID_4L).setRefreshPolicy(IMMEDIATE).setSource(SONGS[3].asMap()).get(); + } + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + client.assignRoleToUser(LIMITED_USER.getName(), LIMITED_ROLE.getName()).assertStatusCode(200); + client.assignRoleToUser(DLS_USER_ROCK.getName(), DLS_ROLE_ROCK.getName()).assertStatusCode(200); + client.assignRoleToUser(DLS_USER_JAZZ.getName(), DLS_ROLE_JAZZ.getName()).assertStatusCode(200); + client.assignRoleToUser(FLS_INCLUDE_TITLE_USER.getName(), FLS_INCLUDE_TITLE_ROLE.getName()).assertStatusCode(200); + client.assignRoleToUser(FLS_EXCLUDE_LYRICS_USER.getName(), FLS_EXCLUDE_LYRICS_ROLE.getName()).assertStatusCode(200); + } + } + + @Test + public void shouldFindDocumentOnRemoteCluster_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchAll(REMOTE_SONG_INDEX); + + SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response, numberOfTotalHitsIsEqualTo(2)); + assertThat(response, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, SONG_ID_1R)); + assertThat(response, searchHitsContainDocumentWithId(1, SONG_INDEX_NAME, SONG_ID_6R)); + } + } + + private SearchRequest searchAll(String indexName) { + SearchRequest searchRequest = SearchRequestFactory.searchAll(indexName); + searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); + return searchRequest; + } + + @Test + public void shouldFindDocumentOnRemoteCluster_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":" + PROHIBITED_SONG_INDEX_NAME); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldSearchForDocumentOnRemoteClustersWhenStarIsUsedAsClusterName_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchAll("*" + ":" + SONG_INDEX_NAME); + + SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); + + // only remote documents are found + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response, numberOfTotalHitsIsEqualTo(2)); + assertThat(response, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, SONG_ID_1R)); + assertThat(response, searchHitsContainDocumentWithId(1, SONG_INDEX_NAME, SONG_ID_6R)); + } + } + + @Test + public void shouldSearchForDocumentOnRemoteClustersWhenStarIsUsedAsClusterName_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchAll("*" + ":" + PROHIBITED_SONG_INDEX_NAME); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldSearchForDocumentOnBothClustersWhenIndexOnBothClusterArePointedOut_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = SearchRequestFactory.searchAll(REMOTE_SONG_INDEX, SONG_INDEX_NAME); + searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); + + SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response, numberOfTotalHitsIsEqualTo(3)); + assertThat( + response, + searchHitsContainDocumentsInAnyOrder( + Pair.of(SONG_INDEX_NAME, SONG_ID_1R), + Pair.of(SONG_INDEX_NAME, SONG_ID_2L), + Pair.of(SONG_INDEX_NAME, SONG_ID_6R) + ) + ); + } + } + + @Test + public void shouldSearchForDocumentOnBothClustersWhenIndexOnBothClusterArePointedOut_negativeLackOfLocalAccess() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + var searchRequest = SearchRequestFactory.searchAll(REMOTE_SONG_INDEX, PROHIBITED_SONG_INDEX_NAME); + searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldSearchForDocumentOnBothClustersWhenIndexOnBothClusterArePointedOut_negativeLackOfRemoteAccess() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + String remoteIndex = REMOTE_CLUSTER_NAME + ":" + PROHIBITED_SONG_INDEX_NAME; + SearchRequest searchRequest = SearchRequestFactory.searchAll(remoteIndex, SONG_INDEX_NAME); + searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldSearchViaAllAliasOnRemoteCluster_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":_all"); + + SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response, numberOfTotalHitsIsEqualTo(4)); + assertThat( + response, + searchHitsContainDocumentsInAnyOrder( + Pair.of(SONG_INDEX_NAME, SONG_ID_1R), + Pair.of(SONG_INDEX_NAME, SONG_ID_6R), + Pair.of(PROHIBITED_SONG_INDEX_NAME, SONG_ID_3R), + Pair.of(LIMITED_USER_INDEX_NAME, SONG_ID_5R) + ) + ); + } + } + + @Test + public void shouldSearchViaAllAliasOnRemoteCluster_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":_all"); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldSearchAllIndexOnRemoteClusterWhenStarIsUsedAsIndexName_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":*"); + + SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response, numberOfTotalHitsIsEqualTo(4)); + assertThat( + response, + searchHitsContainDocumentsInAnyOrder( + Pair.of(SONG_INDEX_NAME, SONG_ID_1R), + Pair.of(SONG_INDEX_NAME, SONG_ID_6R), + Pair.of(PROHIBITED_SONG_INDEX_NAME, SONG_ID_3R), + Pair.of(LIMITED_USER_INDEX_NAME, SONG_ID_5R) + ) + ); + } + } + + @Test + public void shouldSearchAllIndexOnRemoteClusterWhenStarIsUsedAsIndexName_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":*"); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldResolveUserNameExpressionInRoleIndexPattern_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":" + LIMITED_USER_INDEX_NAME); + + SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(response, numberOfTotalHitsIsEqualTo(1)); + } + } + + @Test + public void shouldResolveUserNameExpressionInRoleIndexPattern_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":" + ADMIN_USER_INDEX_NAME); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldSearchInIndexWithPrefix_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":song*"); + + SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response, numberOfTotalHitsIsEqualTo(2)); + assertThat( + response, + searchHitsContainDocumentsInAnyOrder(Pair.of(SONG_INDEX_NAME, SONG_ID_1R), Pair.of(SONG_INDEX_NAME, SONG_ID_6R)) + ); + } + } + + @Test + public void shouldSearchInIndexWithPrefix_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchAll(REMOTE_CLUSTER_NAME + ":prohibited*"); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldEvaluateDocumentLevelSecurityRulesOnRemoteClusterOnSearchRequest_caseRock() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DLS_USER_ROCK)) { + SearchRequest searchRequest = searchAll(REMOTE_SONG_INDEX); + + SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); + + // searching for all documents, so is it important that result contain only one document with id SONG_ID_1 + // and document with SONG_ID_6 is excluded from result set by DLS + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response, numberOfTotalHitsIsEqualTo(1)); + assertThat(response, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, SONG_ID_1R)); + } + } + + @Test + public void shouldEvaluateDocumentLevelSecurityRulesOnRemoteClusterOnSearchRequest_caseJazz() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DLS_USER_JAZZ)) { + SearchRequest searchRequest = searchAll(REMOTE_SONG_INDEX); + + SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); + + // searching for all documents, so is it important that result contain only one document with id SONG_ID_6 + // and document with SONG_ID_1 is excluded from result set by DLS + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response, numberOfTotalHitsIsEqualTo(1)); + assertThat(response, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, SONG_ID_6R)); + } + } + + @Test + public void shouldHaveAccessOnlyToSpecificField() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(FLS_INCLUDE_TITLE_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(REMOTE_SONG_INDEX, QUERY_TITLE_MAGNUM_OPUS); + searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); + + SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response, numberOfTotalHitsIsEqualTo(1)); + // document should contain only title field + assertThat(response, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + assertThat(response, searchHitDoesNotContainField(0, FIELD_ARTIST)); + assertThat(response, searchHitDoesNotContainField(0, FIELD_LYRICS)); + assertThat(response, searchHitDoesNotContainField(0, FIELD_STARS)); + assertThat(response, searchHitDoesNotContainField(0, FIELD_GENRE)); + } + } + + @Test + public void shouldLackAccessToSpecificField() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(FLS_EXCLUDE_LYRICS_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(REMOTE_SONG_INDEX, QUERY_TITLE_MAGNUM_OPUS); + searchRequest.setCcsMinimizeRoundtrips(ccsMinimizeRoundtrips); + + SearchResponse response = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response, numberOfTotalHitsIsEqualTo(1)); + // document should not contain lyrics field + assertThat(response, searchHitDoesNotContainField(0, FIELD_LYRICS)); + + assertThat(response, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + assertThat(response, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_FIRST)); + assertThat(response, searchHitContainsFieldWithValue(0, FIELD_STARS, 1)); + assertThat(response, searchHitContainsFieldWithValue(0, FIELD_GENRE, GENRE_ROCK)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java index 589fe798d5..a9f6cf9b1e 100644 --- a/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java +++ b/src/integrationTest/java/org/opensearch/security/DefaultConfigurationTests.java @@ -37,44 +37,42 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class DefaultConfigurationTests { - private final static Path configurationFolder = ConfigurationFiles.createConfigurationDirectory(); - public static final String ADMIN_USER_NAME = "admin"; - public static final String DEFAULT_PASSWORD = "secret"; - public static final String NEW_USER = "new-user"; - public static final String LIMITED_USER = "limited-user"; + private final static Path configurationFolder = ConfigurationFiles.createConfigurationDirectory(); + public static final String ADMIN_USER_NAME = "admin"; + public static final String DEFAULT_PASSWORD = "secret"; + public static final String NEW_USER = "new-user"; + public static final String LIMITED_USER = "limited-user"; - @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.SINGLENODE) - .nodeSettings(Map.of( - "plugins.security.allow_default_init_securityindex", true, - "plugins.security.restapi.roles_enabled", List.of("user_admin__all_access") - )) - .defaultConfigurationInitDirectory(configurationFolder.toString()) - .loadConfigurationIntoIndex(false) - .build(); + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .nodeSettings( + Map.of( + "plugins.security.allow_default_init_securityindex", + true, + "plugins.security.restapi.roles_enabled", + List.of("user_admin__all_access") + ) + ) + .defaultConfigurationInitDirectory(configurationFolder.toString()) + .loadConfigurationIntoIndex(false) + .build(); - @AfterClass - public static void cleanConfigurationDirectory() throws IOException { - FileUtils.deleteDirectory(configurationFolder.toFile()); - } + @AfterClass + public static void cleanConfigurationDirectory() throws IOException { + FileUtils.deleteDirectory(configurationFolder.toFile()); + } - @Test - public void shouldLoadDefaultConfiguration() { - try(TestRestClient client = cluster.getRestClient(NEW_USER, DEFAULT_PASSWORD)) { - Awaitility.await().alias("Load default configuration") - .until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); - } - try(TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)){ - client.assertCorrectCredentials(ADMIN_USER_NAME); - HttpResponse response = client.get("/_plugins/_security/api/internalusers"); - response.assertStatusCode(200); - Map users = response.getBodyAs(Map.class); - assertThat(users, allOf( - aMapWithSize(3), - hasKey(ADMIN_USER_NAME), - hasKey(NEW_USER), - hasKey(LIMITED_USER))); - } - } + @Test + public void shouldLoadDefaultConfiguration() { + try (TestRestClient client = cluster.getRestClient(NEW_USER, DEFAULT_PASSWORD)) { + Awaitility.await().alias("Load default configuration").until(() -> client.getAuthInfo().getStatusCode(), equalTo(200)); + } + try (TestRestClient client = cluster.getRestClient(ADMIN_USER_NAME, DEFAULT_PASSWORD)) { + client.assertCorrectCredentials(ADMIN_USER_NAME); + HttpResponse response = client.get("/_plugins/_security/api/internalusers"); + response.assertStatusCode(200); + Map users = response.getBodyAs(Map.class); + assertThat(users, allOf(aMapWithSize(3), hasKey(ADMIN_USER_NAME), hasKey(NEW_USER), hasKey(LIMITED_USER))); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/DlsIntegrationTests.java b/src/integrationTest/java/org/opensearch/security/DlsIntegrationTests.java index a99d584a50..d1957e50a6 100644 --- a/src/integrationTest/java/org/opensearch/security/DlsIntegrationTests.java +++ b/src/integrationTest/java/org/opensearch/security/DlsIntegrationTests.java @@ -61,419 +61,456 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class DlsIntegrationTests { - static final String FIRST_INDEX_ID_SONG_1 = "INDEX_1_S1"; - static final String FIRST_INDEX_ID_SONG_2 = "INDEX_1_S2"; - static final String FIRST_INDEX_ID_SONG_3 = "INDEX_1_S3"; - static final String FIRST_INDEX_ID_SONG_4 = "INDEX_1_S4"; - static final String FIRST_INDEX_ID_SONG_5 = "INDEX_1_S5"; - static final String FIRST_INDEX_ID_SONG_6 = "INDEX_1_S6"; - static final String SECOND_INDEX_ID_SONG_1 = "INDEX_2_S1"; - static final String SECOND_INDEX_ID_SONG_2 = "INDEX_2_S2"; - static final String SECOND_INDEX_ID_SONG_3 = "INDEX_2_S3"; - static final String SECOND_INDEX_ID_SONG_4 = "INDEX_2_S4"; - - static final String INDEX_NAME_SUFFIX = "-test-index"; - static final String FIRST_INDEX_NAME = "first".concat(INDEX_NAME_SUFFIX); - static final String SECOND_INDEX_NAME = "second".concat(INDEX_NAME_SUFFIX); - static final String FIRST_INDEX_ALIAS = FIRST_INDEX_NAME.concat("-alias"); - static final String SECOND_INDEX_ALIAS = SECOND_INDEX_NAME.concat("-alias"); - static final String FIRST_INDEX_ALIAS_FILTERED_BY_NEXT_SONG_TITLE = FIRST_INDEX_NAME.concat("-filtered-by-next-song-title"); - static final String FIRST_INDEX_ALIAS_FILTERED_BY_TWINS_ARTIST = FIRST_INDEX_NAME.concat("-filtered-by-twins-artist"); - static final String FIRST_INDEX_ALIAS_FILTERED_BY_FIRST_ARTIST = FIRST_INDEX_NAME.concat("-filtered-by-first-artist"); - static final String ALL_INDICES_ALIAS = "_all"; - - - static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - - /** - * User who is allowed to read all indices. - */ - static final TestSecurityConfig.User READ_ALL_USER = new TestSecurityConfig.User("read_all_user") - .roles( - new TestSecurityConfig.Role("read_all_user") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .on("*") - ); - - /** - * User who is allowed to see all fields on indices {@link #FIRST_INDEX_NAME} and {@link #SECOND_INDEX_NAME}. - */ - static final TestSecurityConfig.User READ_FIRST_AND_SECOND_USER = new TestSecurityConfig.User("read_first_and_second_user") - .roles( - new TestSecurityConfig.Role("first_index_reader") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .on(FIRST_INDEX_NAME), - new TestSecurityConfig.Role("second_index_reader") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .on(SECOND_INDEX_NAME) - ); - - /** - * User who is allowed to see documents on all indices where value of the {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_STRING}. - */ - static final TestSecurityConfig.User READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_STRING = new TestSecurityConfig.User("read_where_field_artist_matches_artist_string") - .roles( - new TestSecurityConfig.Role("read_where_field_artist_matches_artist_string") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_STRING)) - .on("*") - ); - - /** - * User who is allowed to see documents on all indices where value of the {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_TWINS} OR {@link Song#FIELD_STARS} is greater than five. - */ - static final TestSecurityConfig.User READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_FIELD_STARS_GREATER_THAN_FIVE = new TestSecurityConfig.User("read_where_field_artist_matches_artist_twins_or_field_stars_greater_than_five") - .roles( - new TestSecurityConfig.Role("read_where_field_artist_matches_artist_twins") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_TWINS)) - .on("*"), - new TestSecurityConfig.Role("read_where_field_stars_greater_than_five") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .dls(String.format("{\"range\":{\"%s\":{\"gt\":%d}}}", FIELD_STARS, 5)) - .on("*") - ); - - - /** - * User who is allowed to see documents on indices where value of {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_TWINS} or {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_FIRST}: - */ - static final TestSecurityConfig.User READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_MATCHES_ARTIST_FIRST = new TestSecurityConfig.User("read_where_field_artist_matches_artist_twins_or_artist_first") - .roles( - new TestSecurityConfig.Role("read_where_field_artist_matches_artist_twins") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_TWINS)) - .on(FIRST_INDEX_NAME), - new TestSecurityConfig.Role("read_where_field_artist_matches_artist_first") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_FIRST)) - .on(SECOND_INDEX_NAME) - ); - - /** - * User who is allowed to see documents on all indices where value of the {@link Song#FIELD_STARS} is less than three. - */ - static final TestSecurityConfig.User READ_WHERE_STARS_LESS_THAN_THREE = new TestSecurityConfig.User("read_where_stars_less_than_three") - .roles( - new TestSecurityConfig.Role("read_where_stars_less_than_three") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .dls(String.format("{\"range\":{\"%s\":{\"lt\":%d}}}", FIELD_STARS, 3)) - .on("*") - ); - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(false) - .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .users( - ADMIN_USER, READ_ALL_USER, READ_FIRST_AND_SECOND_USER, READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_STRING, - READ_WHERE_STARS_LESS_THAN_THREE, READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_FIELD_STARS_GREATER_THAN_FIVE, - READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_MATCHES_ARTIST_FIRST - ) - .build(); - - /** - * Function that returns id assigned to song with title equal to given title or throws {@link RuntimeException} - * when no song matches. - */ - static final BiFunction, String, String> FIND_ID_OF_SONG_WITH_TITLE = (map, title) -> map.entrySet() - .stream().filter(entry -> title.equals(entry.getValue().getTitle())) - .findAny() - .map(Map.Entry::getKey) - .orElseThrow(() -> new RuntimeException("Cannot find id of song with title: " + title)); - - /** - * Function that returns id assigned to song with artist equal to given artist or throws {@link RuntimeException} - * when no song matches. - */ - static final BiFunction, String, String> FIND_ID_OF_SONG_WITH_ARTIST = (map, artist) -> map.entrySet() - .stream().filter(entry -> artist.equals(entry.getValue().getArtist())) - .findAny() - .map(Map.Entry::getKey) - .orElseThrow(() -> new RuntimeException("Cannot find id of song with artist: " + artist)); - - static final TreeMap FIRST_INDEX_SONGS_BY_ID = new TreeMap<>() {{ // SONG = (String artist, String title, String lyrics, Integer stars, String genre) - put(FIRST_INDEX_ID_SONG_1, SONGS[0]); // (ARTIST_FIRST, TITLE_MAGNUM_OPUS ,LYRICS_1, 1, GENRE_ROCK) - put(FIRST_INDEX_ID_SONG_2, SONGS[1]); // (ARTIST_STRING, TITLE_SONG_1_PLUS_1, LYRICS_2, 2, GENRE_BLUES), - put(FIRST_INDEX_ID_SONG_3, SONGS[2]); // (ARTIST_TWINS, TITLE_NEXT_SONG, LYRICS_3, 3, GENRE_JAZZ), - put(FIRST_INDEX_ID_SONG_4, SONGS[3]); // (ARTIST_NO, TITLE_POISON, LYRICS_4, 4, GENRE_ROCK), - put(FIRST_INDEX_ID_SONG_5, SONGS[4]); // (ARTIST_YES, TITLE_AFFIRMATIVE,LYRICS_5, 5, GENRE_BLUES), - put(FIRST_INDEX_ID_SONG_6, SONGS[5]); // (ARTIST_UNKNOWN, TITLE_CONFIDENTIAL, LYRICS_6, 6, GENRE_JAZZ) - }}; - - static final TreeMap SECOND_INDEX_SONGS_BY_ID = new TreeMap<>() {{ - put(SECOND_INDEX_ID_SONG_1, SONGS[3]); // (ARTIST_NO, TITLE_POISON, LYRICS_4, 4, GENRE_ROCK), - put(SECOND_INDEX_ID_SONG_2, SONGS[2]); // (ARTIST_TWINS, TITLE_NEXT_SONG, LYRICS_3, 3, GENRE_JAZZ), - put(SECOND_INDEX_ID_SONG_3, SONGS[1]); // (ARTIST_STRING, TITLE_SONG_1_PLUS_1, LYRICS_2, 2, GENRE_BLUES), - put(SECOND_INDEX_ID_SONG_4, SONGS[0]); // (ARTIST_FIRST, TITLE_MAGNUM_OPUS ,LYRICS_1, 1, GENRE_ROCK) - }}; - - @BeforeClass - public static void createTestData() { - try(Client client = cluster.getInternalNodeClient()){ - FIRST_INDEX_SONGS_BY_ID.forEach((id, song) -> { - client.prepareIndex(FIRST_INDEX_NAME).setId(id).setRefreshPolicy(IMMEDIATE).setSource(song.asMap()).get(); - }); - - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD) - .indices(FIRST_INDEX_NAME) - .alias(FIRST_INDEX_ALIAS) - )).actionGet(); - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD) - .index(FIRST_INDEX_NAME) - .alias(FIRST_INDEX_ALIAS_FILTERED_BY_NEXT_SONG_TITLE) - .filter(QueryBuilders.queryStringQuery(QUERY_TITLE_NEXT_SONG)) - )).actionGet(); - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD) - .index(FIRST_INDEX_NAME) - .alias(FIRST_INDEX_ALIAS_FILTERED_BY_TWINS_ARTIST) - .filter(QueryBuilders.queryStringQuery(String.format("%s:%s", FIELD_ARTIST, ARTIST_TWINS))) - )).actionGet(); - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD) - .index(FIRST_INDEX_NAME) - .alias(FIRST_INDEX_ALIAS_FILTERED_BY_FIRST_ARTIST) - .filter(QueryBuilders.queryStringQuery(String.format("%s:%s", FIELD_ARTIST, ARTIST_FIRST))) - )).actionGet(); - - SECOND_INDEX_SONGS_BY_ID.forEach((id, song) -> { - client.prepareIndex(SECOND_INDEX_NAME).setId(id).setRefreshPolicy(IMMEDIATE).setSource(song.asMap()).get(); - }); - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD) - .indices(SECOND_INDEX_NAME) - .alias(SECOND_INDEX_ALIAS) - )).actionGet(); - } - } - - @Test - public void testShouldSearchAll() throws IOException { - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_ALL_USER)) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(6)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_FIRST)); - assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_STRING)); - assertThat(searchResponse, searchHitContainsFieldWithValue(2, FIELD_ARTIST, ARTIST_TWINS)); - assertThat(searchResponse, searchHitContainsFieldWithValue(3, FIELD_ARTIST, ARTIST_NO)); - assertThat(searchResponse, searchHitContainsFieldWithValue(4, FIELD_ARTIST, ARTIST_YES)); - assertThat(searchResponse, searchHitContainsFieldWithValue(5, FIELD_ARTIST, ARTIST_UNKNOWN)); - - searchRequest = new SearchRequest(SECOND_INDEX_NAME); - searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_NO)); - } - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_FIRST_AND_SECOND_USER)) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(6)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_FIRST)); - assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_STRING)); - assertThat(searchResponse, searchHitContainsFieldWithValue(2, FIELD_ARTIST, ARTIST_TWINS)); - assertThat(searchResponse, searchHitContainsFieldWithValue(3, FIELD_ARTIST, ARTIST_NO)); - assertThat(searchResponse, searchHitContainsFieldWithValue(4, FIELD_ARTIST, ARTIST_YES)); - assertThat(searchResponse, searchHitContainsFieldWithValue(5, FIELD_ARTIST, ARTIST_UNKNOWN)); - - searchRequest = new SearchRequest(SECOND_INDEX_NAME); - searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_NO)); - } - } - - @Test - public void testShouldSearchI1_S2I2_S3() throws IOException { - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_STRING)) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_STRING)); - - searchRequest = new SearchRequest(SECOND_INDEX_NAME); - searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_STRING)); - } - } - - public void testShouldSearchI1_S3I1_S6I2_S2() throws IOException { - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_FIELD_STARS_GREATER_THAN_FIVE)) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(2)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_TWINS)); - assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_UNKNOWN)); - - searchRequest = new SearchRequest(SECOND_INDEX_NAME); - searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_TWINS)); - } - } - - public void testShouldSearchI1_S1I1_S3I2_S2I2_S4() throws IOException { - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_MATCHES_ARTIST_FIRST)) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(2)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_TWINS)); - assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_FIRST)); - - searchRequest = new SearchRequest(SECOND_INDEX_NAME); - searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(2)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_TWINS)); - assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_FIRST)); - } - } - - public void testShouldSearchStarsLessThanThree() throws IOException { - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_WHERE_STARS_LESS_THAN_THREE)) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(2)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_FIRST)); - assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_STRING)); - - searchRequest = new SearchRequest(SECOND_INDEX_NAME); - searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(2)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_STRING)); - assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_FIRST)); - } - } - - - @Test - public void testSearchForAllDocumentsWithIndexPattern() throws IOException { - - //DLS - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_ALL_USER)) { - SearchRequest searchRequest = new SearchRequest("*".concat(FIRST_INDEX_NAME)); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(6)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_FIRST)); - assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_STRING)); - assertThat(searchResponse, searchHitContainsFieldWithValue(2, FIELD_ARTIST, ARTIST_TWINS)); - assertThat(searchResponse, searchHitContainsFieldWithValue(3, FIELD_ARTIST, ARTIST_NO)); - assertThat(searchResponse, searchHitContainsFieldWithValue(4, FIELD_ARTIST, ARTIST_YES)); - assertThat(searchResponse, searchHitContainsFieldWithValue(5, FIELD_ARTIST, ARTIST_UNKNOWN)); - - searchRequest = new SearchRequest("*".concat(SECOND_INDEX_NAME)); - searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_NO)); - } - } - - @Test - public void testSearchForAllDocumentsWithAlias() throws IOException { - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_ALL_USER)) { - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_ALIAS); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(6)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_FIRST)); - assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_STRING)); - assertThat(searchResponse, searchHitContainsFieldWithValue(2, FIELD_ARTIST, ARTIST_TWINS)); - assertThat(searchResponse, searchHitContainsFieldWithValue(3, FIELD_ARTIST, ARTIST_NO)); - assertThat(searchResponse, searchHitContainsFieldWithValue(4, FIELD_ARTIST, ARTIST_YES)); - assertThat(searchResponse, searchHitContainsFieldWithValue(5, FIELD_ARTIST, ARTIST_UNKNOWN)); - - searchRequest = new SearchRequest("*".concat(SECOND_INDEX_NAME)); - searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_NO)); - } - } - - @Test - public void testAggregateAndComputeStarRatings() throws IOException { - - //DLS - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_MATCHES_ARTIST_FIRST)) { - String aggregationName = "averageStars"; - Song song = FIRST_INDEX_SONGS_BY_ID.get(FIND_ID_OF_SONG_WITH_ARTIST.apply(FIRST_INDEX_SONGS_BY_ID, ARTIST_TWINS)); - - SearchRequest searchRequest = averageAggregationRequest(FIRST_INDEX_NAME, aggregationName, FIELD_STARS); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); - Aggregation actualAggregation = searchResponse.getAggregations().get(aggregationName); - assertThat(actualAggregation, instanceOf(ParsedAvg.class)); - assertThat(((ParsedAvg) actualAggregation).getValue(), is(song.getStars()*1.0)); - } - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_FIELD_STARS_GREATER_THAN_FIVE)) { - String aggregationName = "averageStars"; - SearchRequest searchRequest = averageAggregationRequest(FIRST_INDEX_NAME, aggregationName, FIELD_STARS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); - Aggregation actualAggregation = searchResponse.getAggregations().get(aggregationName); - assertThat(actualAggregation, instanceOf(ParsedAvg.class)); - assertThat(((ParsedAvg) actualAggregation).getValue(), is(4.5)); - } - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_WHERE_STARS_LESS_THAN_THREE)) { - String aggregationName = "averageStars"; - SearchRequest searchRequest = averageAggregationRequest(FIRST_INDEX_NAME, aggregationName, FIELD_STARS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); - Aggregation actualAggregation = searchResponse.getAggregations().get(aggregationName); - assertThat(actualAggregation, instanceOf(ParsedAvg.class)); - assertThat(((ParsedAvg) actualAggregation).getValue(), is(1.5)); - } - } + static final String FIRST_INDEX_ID_SONG_1 = "INDEX_1_S1"; + static final String FIRST_INDEX_ID_SONG_2 = "INDEX_1_S2"; + static final String FIRST_INDEX_ID_SONG_3 = "INDEX_1_S3"; + static final String FIRST_INDEX_ID_SONG_4 = "INDEX_1_S4"; + static final String FIRST_INDEX_ID_SONG_5 = "INDEX_1_S5"; + static final String FIRST_INDEX_ID_SONG_6 = "INDEX_1_S6"; + static final String SECOND_INDEX_ID_SONG_1 = "INDEX_2_S1"; + static final String SECOND_INDEX_ID_SONG_2 = "INDEX_2_S2"; + static final String SECOND_INDEX_ID_SONG_3 = "INDEX_2_S3"; + static final String SECOND_INDEX_ID_SONG_4 = "INDEX_2_S4"; + + static final String INDEX_NAME_SUFFIX = "-test-index"; + static final String FIRST_INDEX_NAME = "first".concat(INDEX_NAME_SUFFIX); + static final String SECOND_INDEX_NAME = "second".concat(INDEX_NAME_SUFFIX); + static final String FIRST_INDEX_ALIAS = FIRST_INDEX_NAME.concat("-alias"); + static final String SECOND_INDEX_ALIAS = SECOND_INDEX_NAME.concat("-alias"); + static final String FIRST_INDEX_ALIAS_FILTERED_BY_NEXT_SONG_TITLE = FIRST_INDEX_NAME.concat("-filtered-by-next-song-title"); + static final String FIRST_INDEX_ALIAS_FILTERED_BY_TWINS_ARTIST = FIRST_INDEX_NAME.concat("-filtered-by-twins-artist"); + static final String FIRST_INDEX_ALIAS_FILTERED_BY_FIRST_ARTIST = FIRST_INDEX_NAME.concat("-filtered-by-first-artist"); + static final String ALL_INDICES_ALIAS = "_all"; + + static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + /** + * User who is allowed to read all indices. + */ + static final TestSecurityConfig.User READ_ALL_USER = new TestSecurityConfig.User("read_all_user").roles( + new TestSecurityConfig.Role("read_all_user").clusterPermissions("cluster_composite_ops_ro").indexPermissions("read").on("*") + ); + + /** + * User who is allowed to see all fields on indices {@link #FIRST_INDEX_NAME} and {@link #SECOND_INDEX_NAME}. + */ + static final TestSecurityConfig.User READ_FIRST_AND_SECOND_USER = new TestSecurityConfig.User("read_first_and_second_user").roles( + new TestSecurityConfig.Role("first_index_reader").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .on(FIRST_INDEX_NAME), + new TestSecurityConfig.Role("second_index_reader").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .on(SECOND_INDEX_NAME) + ); + + /** + * User who is allowed to see documents on all indices where value of the {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_STRING}. + */ + static final TestSecurityConfig.User READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_STRING = new TestSecurityConfig.User( + "read_where_field_artist_matches_artist_string" + ).roles( + new TestSecurityConfig.Role("read_where_field_artist_matches_artist_string").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_STRING)) + .on("*") + ); + + /** + * User who is allowed to see documents on all indices where value of the {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_TWINS} OR {@link Song#FIELD_STARS} is greater than five. + */ + static final TestSecurityConfig.User READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_FIELD_STARS_GREATER_THAN_FIVE = + new TestSecurityConfig.User("read_where_field_artist_matches_artist_twins_or_field_stars_greater_than_five").roles( + new TestSecurityConfig.Role("read_where_field_artist_matches_artist_twins").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_TWINS)) + .on("*"), + new TestSecurityConfig.Role("read_where_field_stars_greater_than_five").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .dls(String.format("{\"range\":{\"%s\":{\"gt\":%d}}}", FIELD_STARS, 5)) + .on("*") + ); + + /** + * User who is allowed to see documents on indices where value of {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_TWINS} or {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_FIRST}: + */ + static final TestSecurityConfig.User READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_MATCHES_ARTIST_FIRST = new TestSecurityConfig.User( + "read_where_field_artist_matches_artist_twins_or_artist_first" + ).roles( + new TestSecurityConfig.Role("read_where_field_artist_matches_artist_twins").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_TWINS)) + .on(FIRST_INDEX_NAME), + new TestSecurityConfig.Role("read_where_field_artist_matches_artist_first").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_FIRST)) + .on(SECOND_INDEX_NAME) + ); + + /** + * User who is allowed to see documents on all indices where value of the {@link Song#FIELD_STARS} is less than three. + */ + static final TestSecurityConfig.User READ_WHERE_STARS_LESS_THAN_THREE = new TestSecurityConfig.User("read_where_stars_less_than_three") + .roles( + new TestSecurityConfig.Role("read_where_stars_less_than_three").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .dls(String.format("{\"range\":{\"%s\":{\"lt\":%d}}}", FIELD_STARS, 3)) + .on("*") + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .anonymousAuth(false) + .nodeSettings( + Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName())) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users( + ADMIN_USER, + READ_ALL_USER, + READ_FIRST_AND_SECOND_USER, + READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_STRING, + READ_WHERE_STARS_LESS_THAN_THREE, + READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_FIELD_STARS_GREATER_THAN_FIVE, + READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_MATCHES_ARTIST_FIRST + ) + .build(); + + /** + * Function that returns id assigned to song with title equal to given title or throws {@link RuntimeException} + * when no song matches. + */ + static final BiFunction, String, String> FIND_ID_OF_SONG_WITH_TITLE = (map, title) -> map.entrySet() + .stream() + .filter(entry -> title.equals(entry.getValue().getTitle())) + .findAny() + .map(Map.Entry::getKey) + .orElseThrow(() -> new RuntimeException("Cannot find id of song with title: " + title)); + + /** + * Function that returns id assigned to song with artist equal to given artist or throws {@link RuntimeException} + * when no song matches. + */ + static final BiFunction, String, String> FIND_ID_OF_SONG_WITH_ARTIST = (map, artist) -> map.entrySet() + .stream() + .filter(entry -> artist.equals(entry.getValue().getArtist())) + .findAny() + .map(Map.Entry::getKey) + .orElseThrow(() -> new RuntimeException("Cannot find id of song with artist: " + artist)); + + static final TreeMap FIRST_INDEX_SONGS_BY_ID = new TreeMap<>() { + { // SONG = (String artist, String title, String lyrics, Integer stars, String genre) + put(FIRST_INDEX_ID_SONG_1, SONGS[0]); // (ARTIST_FIRST, TITLE_MAGNUM_OPUS ,LYRICS_1, 1, GENRE_ROCK) + put(FIRST_INDEX_ID_SONG_2, SONGS[1]); // (ARTIST_STRING, TITLE_SONG_1_PLUS_1, LYRICS_2, 2, GENRE_BLUES), + put(FIRST_INDEX_ID_SONG_3, SONGS[2]); // (ARTIST_TWINS, TITLE_NEXT_SONG, LYRICS_3, 3, GENRE_JAZZ), + put(FIRST_INDEX_ID_SONG_4, SONGS[3]); // (ARTIST_NO, TITLE_POISON, LYRICS_4, 4, GENRE_ROCK), + put(FIRST_INDEX_ID_SONG_5, SONGS[4]); // (ARTIST_YES, TITLE_AFFIRMATIVE,LYRICS_5, 5, GENRE_BLUES), + put(FIRST_INDEX_ID_SONG_6, SONGS[5]); // (ARTIST_UNKNOWN, TITLE_CONFIDENTIAL, LYRICS_6, 6, GENRE_JAZZ) + } + }; + + static final TreeMap SECOND_INDEX_SONGS_BY_ID = new TreeMap<>() { + { + put(SECOND_INDEX_ID_SONG_1, SONGS[3]); // (ARTIST_NO, TITLE_POISON, LYRICS_4, 4, GENRE_ROCK), + put(SECOND_INDEX_ID_SONG_2, SONGS[2]); // (ARTIST_TWINS, TITLE_NEXT_SONG, LYRICS_3, 3, GENRE_JAZZ), + put(SECOND_INDEX_ID_SONG_3, SONGS[1]); // (ARTIST_STRING, TITLE_SONG_1_PLUS_1, LYRICS_2, 2, GENRE_BLUES), + put(SECOND_INDEX_ID_SONG_4, SONGS[0]); // (ARTIST_FIRST, TITLE_MAGNUM_OPUS ,LYRICS_1, 1, GENRE_ROCK) + } + }; + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + FIRST_INDEX_SONGS_BY_ID.forEach((id, song) -> { + client.prepareIndex(FIRST_INDEX_NAME).setId(id).setRefreshPolicy(IMMEDIATE).setSource(song.asMap()).get(); + }); + + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).indices(FIRST_INDEX_NAME).alias(FIRST_INDEX_ALIAS) + ) + ) + .actionGet(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).index(FIRST_INDEX_NAME) + .alias(FIRST_INDEX_ALIAS_FILTERED_BY_NEXT_SONG_TITLE) + .filter(QueryBuilders.queryStringQuery(QUERY_TITLE_NEXT_SONG)) + ) + ) + .actionGet(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).index(FIRST_INDEX_NAME) + .alias(FIRST_INDEX_ALIAS_FILTERED_BY_TWINS_ARTIST) + .filter(QueryBuilders.queryStringQuery(String.format("%s:%s", FIELD_ARTIST, ARTIST_TWINS))) + ) + ) + .actionGet(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).index(FIRST_INDEX_NAME) + .alias(FIRST_INDEX_ALIAS_FILTERED_BY_FIRST_ARTIST) + .filter(QueryBuilders.queryStringQuery(String.format("%s:%s", FIELD_ARTIST, ARTIST_FIRST))) + ) + ) + .actionGet(); + + SECOND_INDEX_SONGS_BY_ID.forEach((id, song) -> { + client.prepareIndex(SECOND_INDEX_NAME).setId(id).setRefreshPolicy(IMMEDIATE).setSource(song.asMap()).get(); + }); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).indices(SECOND_INDEX_NAME).alias(SECOND_INDEX_ALIAS) + ) + ) + .actionGet(); + } + } + + @Test + public void testShouldSearchAll() throws IOException { + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_ALL_USER)) { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(6)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_FIRST)); + assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_STRING)); + assertThat(searchResponse, searchHitContainsFieldWithValue(2, FIELD_ARTIST, ARTIST_TWINS)); + assertThat(searchResponse, searchHitContainsFieldWithValue(3, FIELD_ARTIST, ARTIST_NO)); + assertThat(searchResponse, searchHitContainsFieldWithValue(4, FIELD_ARTIST, ARTIST_YES)); + assertThat(searchResponse, searchHitContainsFieldWithValue(5, FIELD_ARTIST, ARTIST_UNKNOWN)); + + searchRequest = new SearchRequest(SECOND_INDEX_NAME); + searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_NO)); + } + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_FIRST_AND_SECOND_USER)) { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(6)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_FIRST)); + assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_STRING)); + assertThat(searchResponse, searchHitContainsFieldWithValue(2, FIELD_ARTIST, ARTIST_TWINS)); + assertThat(searchResponse, searchHitContainsFieldWithValue(3, FIELD_ARTIST, ARTIST_NO)); + assertThat(searchResponse, searchHitContainsFieldWithValue(4, FIELD_ARTIST, ARTIST_YES)); + assertThat(searchResponse, searchHitContainsFieldWithValue(5, FIELD_ARTIST, ARTIST_UNKNOWN)); + + searchRequest = new SearchRequest(SECOND_INDEX_NAME); + searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_NO)); + } + } + + @Test + public void testShouldSearchI1_S2I2_S3() throws IOException { + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_STRING)) { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_STRING)); + + searchRequest = new SearchRequest(SECOND_INDEX_NAME); + searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_STRING)); + } + } + + public void testShouldSearchI1_S3I1_S6I2_S2() throws IOException { + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_FIELD_STARS_GREATER_THAN_FIVE + ) + ) { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(2)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_TWINS)); + assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_UNKNOWN)); + + searchRequest = new SearchRequest(SECOND_INDEX_NAME); + searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_TWINS)); + } + } + + public void testShouldSearchI1_S1I1_S3I2_S2I2_S4() throws IOException { + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_MATCHES_ARTIST_FIRST + ) + ) { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(2)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_TWINS)); + assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_FIRST)); + + searchRequest = new SearchRequest(SECOND_INDEX_NAME); + searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(2)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_TWINS)); + assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_FIRST)); + } + } + + public void testShouldSearchStarsLessThanThree() throws IOException { + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_WHERE_STARS_LESS_THAN_THREE)) { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_NAME); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(2)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_FIRST)); + assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_STRING)); + + searchRequest = new SearchRequest(SECOND_INDEX_NAME); + searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(2)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_STRING)); + assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_FIRST)); + } + } + + @Test + public void testSearchForAllDocumentsWithIndexPattern() throws IOException { + + // DLS + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_ALL_USER)) { + SearchRequest searchRequest = new SearchRequest("*".concat(FIRST_INDEX_NAME)); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(6)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_FIRST)); + assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_STRING)); + assertThat(searchResponse, searchHitContainsFieldWithValue(2, FIELD_ARTIST, ARTIST_TWINS)); + assertThat(searchResponse, searchHitContainsFieldWithValue(3, FIELD_ARTIST, ARTIST_NO)); + assertThat(searchResponse, searchHitContainsFieldWithValue(4, FIELD_ARTIST, ARTIST_YES)); + assertThat(searchResponse, searchHitContainsFieldWithValue(5, FIELD_ARTIST, ARTIST_UNKNOWN)); + + searchRequest = new SearchRequest("*".concat(SECOND_INDEX_NAME)); + searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_NO)); + } + } + + @Test + public void testSearchForAllDocumentsWithAlias() throws IOException { + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_ALL_USER)) { + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_ALIAS); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(6)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_FIRST)); + assertThat(searchResponse, searchHitContainsFieldWithValue(1, FIELD_ARTIST, ARTIST_STRING)); + assertThat(searchResponse, searchHitContainsFieldWithValue(2, FIELD_ARTIST, ARTIST_TWINS)); + assertThat(searchResponse, searchHitContainsFieldWithValue(3, FIELD_ARTIST, ARTIST_NO)); + assertThat(searchResponse, searchHitContainsFieldWithValue(4, FIELD_ARTIST, ARTIST_YES)); + assertThat(searchResponse, searchHitContainsFieldWithValue(5, FIELD_ARTIST, ARTIST_UNKNOWN)); + + searchRequest = new SearchRequest("*".concat(SECOND_INDEX_NAME)); + searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(4)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, ARTIST_NO)); + } + } + + @Test + public void testAggregateAndComputeStarRatings() throws IOException { + + // DLS + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_MATCHES_ARTIST_FIRST + ) + ) { + String aggregationName = "averageStars"; + Song song = FIRST_INDEX_SONGS_BY_ID.get(FIND_ID_OF_SONG_WITH_ARTIST.apply(FIRST_INDEX_SONGS_BY_ID, ARTIST_TWINS)); + + SearchRequest searchRequest = averageAggregationRequest(FIRST_INDEX_NAME, aggregationName, FIELD_STARS); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); + Aggregation actualAggregation = searchResponse.getAggregations().get(aggregationName); + assertThat(actualAggregation, instanceOf(ParsedAvg.class)); + assertThat(((ParsedAvg) actualAggregation).getValue(), is(song.getStars() * 1.0)); + } + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + READ_WHERE_FIELD_ARTIST_MATCHES_ARTIST_TWINS_OR_FIELD_STARS_GREATER_THAN_FIVE + ) + ) { + String aggregationName = "averageStars"; + SearchRequest searchRequest = averageAggregationRequest(FIRST_INDEX_NAME, aggregationName, FIELD_STARS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); + Aggregation actualAggregation = searchResponse.getAggregations().get(aggregationName); + assertThat(actualAggregation, instanceOf(ParsedAvg.class)); + assertThat(((ParsedAvg) actualAggregation).getValue(), is(4.5)); + } + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(READ_WHERE_STARS_LESS_THAN_THREE)) { + String aggregationName = "averageStars"; + SearchRequest searchRequest = averageAggregationRequest(FIRST_INDEX_NAME, aggregationName, FIELD_STARS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); + Aggregation actualAggregation = searchResponse.getAggregations().get(aggregationName); + assertThat(actualAggregation, instanceOf(ParsedAvg.class)); + assertThat(((ParsedAvg) actualAggregation).getValue(), is(1.5)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java b/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java index 67d61b7d6f..f3b641f1e6 100644 --- a/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java +++ b/src/integrationTest/java/org/opensearch/security/DoNotFailOnForbiddenTests.java @@ -80,301 +80,332 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class DoNotFailOnForbiddenTests { - /** - * Songs accessible for {@link #LIMITED_USER} - */ - private static final String MARVELOUS_SONGS = "marvelous_songs"; - - /** - * Songs inaccessible for {@link #LIMITED_USER} - */ - private static final String HORRIBLE_SONGS = "horrible_songs"; - - private static final String BOTH_INDEX_PATTERN = "*songs"; - - private static final String ID_1 = "1"; - private static final String ID_2 = "2"; - private static final String ID_3 = "3"; - private static final String ID_4 = "4"; - - private static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS); - private static final User LIMITED_USER = new User("limited_user") - .roles(new TestSecurityConfig.Role("limited-role") - .clusterPermissions("indices:data/read/mget", "indices:data/read/msearch", "indices:data/read/scroll") - .indexPermissions("indices:data/read/search", "indices:data/read/mget*", "indices:data/read/field_caps", "indices:data/read/field_caps*", "indices:data/read/msearch", "indices:data/read/scroll") - .on(MARVELOUS_SONGS)); - - private static final String BOTH_INDEX_ALIAS = "both-indices"; - private static final String FORBIDDEN_INDEX_ALIAS = "forbidden-index"; - - @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).authc(AUTHC_HTTPBASIC_INTERNAL) - .users(ADMIN_USER, LIMITED_USER).anonymousAuth(false).doNotFailOnForbidden(true).build(); - - @BeforeClass - public static void createTestData() { - try(Client client = cluster.getInternalNodeClient()) { - client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(MARVELOUS_SONGS).id(ID_1).source(SONGS[0].asMap())).actionGet(); - client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(MARVELOUS_SONGS).id(ID_2).source(SONGS[1].asMap())).actionGet(); - client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(MARVELOUS_SONGS).id(ID_3).source(SONGS[2].asMap())).actionGet(); - - client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(HORRIBLE_SONGS).id(ID_4).source(SONGS[3].asMap())).actionGet(); - - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD).indices( - MARVELOUS_SONGS, HORRIBLE_SONGS).alias(BOTH_INDEX_ALIAS))).actionGet(); - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD).indices( - HORRIBLE_SONGS).alias(FORBIDDEN_INDEX_ALIAS))).actionGet(); - - } - } - - @Test - public void shouldPerformSimpleSearch_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(new String[]{MARVELOUS_SONGS, HORRIBLE_SONGS}, QUERY_TITLE_MAGNUM_OPUS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThatContainOneSong(searchResponse, ID_1, TITLE_MAGNUM_OPUS); - } - } - - private static void assertThatContainOneSong(SearchResponse searchResponse, String documentId, String title) { - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, MARVELOUS_SONGS, documentId)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, title)); - } - - @Test - public void shouldPerformSimpleSearch_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(HORRIBLE_SONGS, QUERY_TITLE_POISON); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldSearchForDocumentsViaIndexPattern_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(BOTH_INDEX_PATTERN, QUERY_TITLE_MAGNUM_OPUS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThatContainOneSong(searchResponse, ID_1, TITLE_MAGNUM_OPUS); - } - } - - @Test - public void shouldSearchForDocumentsViaIndexPattern_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(HORRIBLE_SONGS, QUERY_TITLE_POISON); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldSearchForDocumentsViaAlias_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(BOTH_INDEX_ALIAS, QUERY_TITLE_MAGNUM_OPUS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThatContainOneSong(searchResponse, ID_1, TITLE_MAGNUM_OPUS); - } - } - - @Test - public void shouldSearchForDocumentsViaAlias_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(FORBIDDEN_INDEX_ALIAS, QUERY_TITLE_POISON); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldSearchForDocumentsViaAll_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = queryStringQueryRequest("_all", QUERY_TITLE_MAGNUM_OPUS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThatContainOneSong(searchResponse, ID_1, TITLE_MAGNUM_OPUS); - } - } - - @Test - public void shouldSearchForDocumentsViaAll_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = queryStringQueryRequest("_all", QUERY_TITLE_POISON); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(0)); - } - } - - @Test - public void shouldMGetDocument_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - MultiGetRequest request = new MultiGetRequest() - .add(BOTH_INDEX_PATTERN, ID_1) - .add(BOTH_INDEX_PATTERN, ID_4); - - MultiGetResponse response = restHighLevelClient.mget(request, DEFAULT); - - MultiGetItemResponse[] responses = response.getResponses(); - assertThat(responses, arrayWithSize(2)); - MultiGetItemResponse firstResult = responses[0]; - MultiGetItemResponse secondResult = responses[1]; - assertThat(firstResult.getFailure(), nullValue()); - assertThat(secondResult.getFailure(), nullValue()); - assertThat(firstResult.getResponse(), allOf( - containDocument(MARVELOUS_SONGS, ID_1), - documentContainField(FIELD_TITLE, TITLE_MAGNUM_OPUS)) - ); - assertThat(secondResult.getResponse(), containOnlyDocumentId(MARVELOUS_SONGS, ID_4)); - } - } - - @Test - public void shouldMGetDocument_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - MultiGetRequest request = new MultiGetRequest().add(HORRIBLE_SONGS, ID_4); - - assertThatThrownBy(() -> restHighLevelClient.mget(request, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldMSearchDocument_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - MultiSearchRequest request = new MultiSearchRequest(); - request.add(queryStringQueryRequest(BOTH_INDEX_PATTERN, QUERY_TITLE_MAGNUM_OPUS)); - request.add(queryStringQueryRequest(BOTH_INDEX_PATTERN, QUERY_TITLE_NEXT_SONG)); - - MultiSearchResponse response = restHighLevelClient.msearch(request, DEFAULT); - - MultiSearchResponse.Item[] responses = response.getResponses(); - assertThat(responses, Matchers.arrayWithSize(2)); - assertThat(responses[0].getFailure(), nullValue()); - assertThat(responses[1].getFailure(), nullValue()); - - assertThat(responses[0].getResponse(), searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); - assertThat(responses[0].getResponse(), searchHitsContainDocumentWithId(0, MARVELOUS_SONGS, ID_1)); - assertThat(responses[1].getResponse(), searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_NEXT_SONG)); - assertThat(responses[1].getResponse(), searchHitsContainDocumentWithId(0, MARVELOUS_SONGS, ID_3)); - } - } - - @Test - public void shouldMSearchDocument_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - MultiSearchRequest request = new MultiSearchRequest(); - request.add(queryStringQueryRequest(FORBIDDEN_INDEX_ALIAS, QUERY_TITLE_POISON)); - - assertThatThrownBy(() -> restHighLevelClient.msearch(request, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldGetFieldCapabilities_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(MARVELOUS_SONGS, HORRIBLE_SONGS).fields(FIELD_TITLE); - - FieldCapabilitiesResponse response = restHighLevelClient.fieldCaps(request, DEFAULT); - - assertThat(response.get(), aMapWithSize(1)); - assertThat(response.getIndices(), arrayWithSize(1)); - assertThat(response.getField(FIELD_TITLE), hasKey("text")); - assertThat(response.getIndices(), arrayContainingInAnyOrder(MARVELOUS_SONGS)); - } - } - - @Test - public void shouldGetFieldCapabilities_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(HORRIBLE_SONGS).fields(FIELD_TITLE); - - assertThatThrownBy(() -> restHighLevelClient.fieldCaps(request, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldScrollOverSearchResults_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = searchRequestWithScroll(BOTH_INDEX_PATTERN, 2); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, containNotEmptyScrollingId()); - - SearchScrollRequest scrollRequest = getSearchScrollRequest(searchResponse); - - SearchResponse scrollResponse = restHighLevelClient.scroll(scrollRequest, DEFAULT); - assertThat(scrollResponse, isSuccessfulSearchResponse()); - assertThat(scrollResponse, containNotEmptyScrollingId()); - assertThat(scrollResponse, numberOfTotalHitsIsEqualTo(3)); - assertThat(scrollResponse, numberOfHitsInPageIsEqualTo(1)); - } - } - - @Test - public void shouldScrollOverSearchResults_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - SearchRequest searchRequest = searchRequestWithScroll(HORRIBLE_SONGS, 2); - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldPerformAggregation_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - final String aggregationName = "averageStars"; - SearchRequest searchRequest = averageAggregationRequest(BOTH_INDEX_PATTERN, aggregationName, FIELD_STARS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); - } - } - - @Test - public void shouldPerformAggregation_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - final String aggregationName = "averageStars"; - SearchRequest searchRequest = averageAggregationRequest(HORRIBLE_SONGS, aggregationName, FIELD_STARS); - - assertThatThrownBy( () -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldPerformStatAggregation_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - final String aggregationName = "statsStars"; - SearchRequest searchRequest = statsAggregationRequest(BOTH_INDEX_ALIAS, aggregationName, FIELD_STARS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "stats")); - } - } - - @Test - public void shouldPerformStatAggregation_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { - final String aggregationName = "statsStars"; - SearchRequest searchRequest = statsAggregationRequest(HORRIBLE_SONGS, aggregationName, FIELD_STARS); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } + /** + * Songs accessible for {@link #LIMITED_USER} + */ + private static final String MARVELOUS_SONGS = "marvelous_songs"; + + /** + * Songs inaccessible for {@link #LIMITED_USER} + */ + private static final String HORRIBLE_SONGS = "horrible_songs"; + + private static final String BOTH_INDEX_PATTERN = "*songs"; + + private static final String ID_1 = "1"; + private static final String ID_2 = "2"; + private static final String ID_3 = "3"; + private static final String ID_4 = "4"; + + private static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS); + private static final User LIMITED_USER = new User("limited_user").roles( + new TestSecurityConfig.Role("limited-role").clusterPermissions( + "indices:data/read/mget", + "indices:data/read/msearch", + "indices:data/read/scroll" + ) + .indexPermissions( + "indices:data/read/search", + "indices:data/read/mget*", + "indices:data/read/field_caps", + "indices:data/read/field_caps*", + "indices:data/read/msearch", + "indices:data/read/scroll" + ) + .on(MARVELOUS_SONGS) + ); + + private static final String BOTH_INDEX_ALIAS = "both-indices"; + private static final String FORBIDDEN_INDEX_ALIAS = "forbidden-index"; + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER, LIMITED_USER) + .anonymousAuth(false) + .doNotFailOnForbidden(true) + .build(); + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(MARVELOUS_SONGS).id(ID_1).source(SONGS[0].asMap())) + .actionGet(); + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(MARVELOUS_SONGS).id(ID_2).source(SONGS[1].asMap())) + .actionGet(); + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(MARVELOUS_SONGS).id(ID_3).source(SONGS[2].asMap())) + .actionGet(); + + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(HORRIBLE_SONGS).id(ID_4).source(SONGS[3].asMap())) + .actionGet(); + + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).indices(MARVELOUS_SONGS, HORRIBLE_SONGS).alias(BOTH_INDEX_ALIAS) + ) + ) + .actionGet(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).indices(HORRIBLE_SONGS).alias(FORBIDDEN_INDEX_ALIAS) + ) + ) + .actionGet(); + + } + } + + @Test + public void shouldPerformSimpleSearch_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest( + new String[] { MARVELOUS_SONGS, HORRIBLE_SONGS }, + QUERY_TITLE_MAGNUM_OPUS + ); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThatContainOneSong(searchResponse, ID_1, TITLE_MAGNUM_OPUS); + } + } + + private static void assertThatContainOneSong(SearchResponse searchResponse, String documentId, String title) { + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, MARVELOUS_SONGS, documentId)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, title)); + } + + @Test + public void shouldPerformSimpleSearch_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(HORRIBLE_SONGS, QUERY_TITLE_POISON); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldSearchForDocumentsViaIndexPattern_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(BOTH_INDEX_PATTERN, QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThatContainOneSong(searchResponse, ID_1, TITLE_MAGNUM_OPUS); + } + } + + @Test + public void shouldSearchForDocumentsViaIndexPattern_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(HORRIBLE_SONGS, QUERY_TITLE_POISON); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldSearchForDocumentsViaAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(BOTH_INDEX_ALIAS, QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThatContainOneSong(searchResponse, ID_1, TITLE_MAGNUM_OPUS); + } + } + + @Test + public void shouldSearchForDocumentsViaAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(FORBIDDEN_INDEX_ALIAS, QUERY_TITLE_POISON); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldSearchForDocumentsViaAll_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("_all", QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThatContainOneSong(searchResponse, ID_1, TITLE_MAGNUM_OPUS); + } + } + + @Test + public void shouldSearchForDocumentsViaAll_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("_all", QUERY_TITLE_POISON); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(0)); + } + } + + @Test + public void shouldMGetDocument_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + MultiGetRequest request = new MultiGetRequest().add(BOTH_INDEX_PATTERN, ID_1).add(BOTH_INDEX_PATTERN, ID_4); + + MultiGetResponse response = restHighLevelClient.mget(request, DEFAULT); + + MultiGetItemResponse[] responses = response.getResponses(); + assertThat(responses, arrayWithSize(2)); + MultiGetItemResponse firstResult = responses[0]; + MultiGetItemResponse secondResult = responses[1]; + assertThat(firstResult.getFailure(), nullValue()); + assertThat(secondResult.getFailure(), nullValue()); + assertThat( + firstResult.getResponse(), + allOf(containDocument(MARVELOUS_SONGS, ID_1), documentContainField(FIELD_TITLE, TITLE_MAGNUM_OPUS)) + ); + assertThat(secondResult.getResponse(), containOnlyDocumentId(MARVELOUS_SONGS, ID_4)); + } + } + + @Test + public void shouldMGetDocument_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + MultiGetRequest request = new MultiGetRequest().add(HORRIBLE_SONGS, ID_4); + + assertThatThrownBy(() -> restHighLevelClient.mget(request, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldMSearchDocument_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + MultiSearchRequest request = new MultiSearchRequest(); + request.add(queryStringQueryRequest(BOTH_INDEX_PATTERN, QUERY_TITLE_MAGNUM_OPUS)); + request.add(queryStringQueryRequest(BOTH_INDEX_PATTERN, QUERY_TITLE_NEXT_SONG)); + + MultiSearchResponse response = restHighLevelClient.msearch(request, DEFAULT); + + MultiSearchResponse.Item[] responses = response.getResponses(); + assertThat(responses, Matchers.arrayWithSize(2)); + assertThat(responses[0].getFailure(), nullValue()); + assertThat(responses[1].getFailure(), nullValue()); + + assertThat(responses[0].getResponse(), searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + assertThat(responses[0].getResponse(), searchHitsContainDocumentWithId(0, MARVELOUS_SONGS, ID_1)); + assertThat(responses[1].getResponse(), searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_NEXT_SONG)); + assertThat(responses[1].getResponse(), searchHitsContainDocumentWithId(0, MARVELOUS_SONGS, ID_3)); + } + } + + @Test + public void shouldMSearchDocument_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + MultiSearchRequest request = new MultiSearchRequest(); + request.add(queryStringQueryRequest(FORBIDDEN_INDEX_ALIAS, QUERY_TITLE_POISON)); + + assertThatThrownBy(() -> restHighLevelClient.msearch(request, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldGetFieldCapabilities_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(MARVELOUS_SONGS, HORRIBLE_SONGS).fields(FIELD_TITLE); + + FieldCapabilitiesResponse response = restHighLevelClient.fieldCaps(request, DEFAULT); + + assertThat(response.get(), aMapWithSize(1)); + assertThat(response.getIndices(), arrayWithSize(1)); + assertThat(response.getField(FIELD_TITLE), hasKey("text")); + assertThat(response.getIndices(), arrayContainingInAnyOrder(MARVELOUS_SONGS)); + } + } + + @Test + public void shouldGetFieldCapabilities_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(HORRIBLE_SONGS).fields(FIELD_TITLE); + + assertThatThrownBy(() -> restHighLevelClient.fieldCaps(request, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldScrollOverSearchResults_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchRequestWithScroll(BOTH_INDEX_PATTERN, 2); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containNotEmptyScrollingId()); + + SearchScrollRequest scrollRequest = getSearchScrollRequest(searchResponse); + + SearchResponse scrollResponse = restHighLevelClient.scroll(scrollRequest, DEFAULT); + assertThat(scrollResponse, isSuccessfulSearchResponse()); + assertThat(scrollResponse, containNotEmptyScrollingId()); + assertThat(scrollResponse, numberOfTotalHitsIsEqualTo(3)); + assertThat(scrollResponse, numberOfHitsInPageIsEqualTo(1)); + } + } + + @Test + public void shouldScrollOverSearchResults_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + SearchRequest searchRequest = searchRequestWithScroll(HORRIBLE_SONGS, 2); + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldPerformAggregation_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + final String aggregationName = "averageStars"; + SearchRequest searchRequest = averageAggregationRequest(BOTH_INDEX_PATTERN, aggregationName, FIELD_STARS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); + } + } + + @Test + public void shouldPerformAggregation_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + final String aggregationName = "averageStars"; + SearchRequest searchRequest = averageAggregationRequest(HORRIBLE_SONGS, aggregationName, FIELD_STARS); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldPerformStatAggregation_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + final String aggregationName = "statsStars"; + SearchRequest searchRequest = statsAggregationRequest(BOTH_INDEX_ALIAS, aggregationName, FIELD_STARS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "stats")); + } + } + + @Test + public void shouldPerformStatAggregation_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_USER)) { + final String aggregationName = "statsStars"; + SearchRequest searchRequest = statsAggregationRequest(HORRIBLE_SONGS, aggregationName, FIELD_STARS); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/FlsAndFieldMaskingTests.java b/src/integrationTest/java/org/opensearch/security/FlsAndFieldMaskingTests.java index 692a3d8e4f..4a5460f329 100644 --- a/src/integrationTest/java/org/opensearch/security/FlsAndFieldMaskingTests.java +++ b/src/integrationTest/java/org/opensearch/security/FlsAndFieldMaskingTests.java @@ -101,671 +101,714 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class FlsAndFieldMaskingTests { - static final String FIRST_INDEX_ID_SONG_1 = "INDEX_1_S1"; - static final String FIRST_INDEX_ID_SONG_2 = "INDEX_1_S2"; - static final String FIRST_INDEX_ID_SONG_3 = "INDEX_1_S3"; - static final String FIRST_INDEX_ID_SONG_4 = "INDEX_1_S4"; - static final String SECOND_INDEX_ID_SONG_1 = "INDEX_2_S1"; - static final String SECOND_INDEX_ID_SONG_2 = "INDEX_2_S2"; - static final String SECOND_INDEX_ID_SONG_3 = "INDEX_2_S3"; - static final String SECOND_INDEX_ID_SONG_4 = "INDEX_2_S4"; - - static final String INDEX_NAME_SUFFIX = "-test-index"; - static final String FIRST_INDEX_NAME = "first".concat(INDEX_NAME_SUFFIX); - static final String SECOND_INDEX_NAME = "second".concat(INDEX_NAME_SUFFIX); - static final String FIRST_INDEX_ALIAS = FIRST_INDEX_NAME.concat("-alias"); - static final String SECOND_INDEX_ALIAS = SECOND_INDEX_NAME.concat("-alias"); - static final String FIRST_INDEX_ALIAS_FILTERED_BY_NEXT_SONG_TITLE = FIRST_INDEX_NAME.concat("-filtered-by-next-song-title"); - static final String FIRST_INDEX_ALIAS_FILTERED_BY_TWINS_ARTIST = FIRST_INDEX_NAME.concat("-filtered-by-twins-artist"); - static final String FIRST_INDEX_ALIAS_FILTERED_BY_FIRST_ARTIST = FIRST_INDEX_NAME.concat("-filtered-by-first-artist"); - static final String ALL_INDICES_ALIAS = "_all"; - - static final String MASK_VALUE = "*"; - - static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - - /** - * User who is allowed to see all fields on all indices. Values of the title and artist fields should be masked. - */ - static final TestSecurityConfig.User ALL_INDICES_MASKED_TITLE_ARTIST_READER = new TestSecurityConfig.User("masked_artist_title_reader") - .roles( - new TestSecurityConfig.Role("masked_artist_title_reader") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .maskedFields( - FIELD_TITLE.concat("::/(?<=.{1})./::").concat(MASK_VALUE), - FIELD_ARTIST.concat("::/(?<=.{1})./::").concat(MASK_VALUE) - ) - .on("*") - ); - - /** - * User who is allowed to see all fields on indices {@link #FIRST_INDEX_NAME} and {@link #SECOND_INDEX_NAME}. - *
    - *
  • values of the artist and lyrics fields should be masked on index {@link #FIRST_INDEX_NAME}
  • - *
  • values of the lyrics field should be masked on index {@link #SECOND_INDEX_NAME}
  • - *
- */ - static final TestSecurityConfig.User MASKED_ARTIST_LYRICS_READER = new TestSecurityConfig.User("masked_title_artist_lyrics_reader") - .roles( - new TestSecurityConfig.Role("masked_title_artist_lyrics_reader") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .maskedFields( - FIELD_ARTIST.concat("::/(?<=.{1})./::").concat(MASK_VALUE), - FIELD_LYRICS.concat("::/(?<=.{1})./::").concat(MASK_VALUE) - ) - .on(FIRST_INDEX_NAME), - new TestSecurityConfig.Role("masked_lyrics_reader") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .maskedFields(FIELD_LYRICS.concat("::/(?<=.{1})./::").concat(MASK_VALUE)) - .on(SECOND_INDEX_NAME) - ); - - /** - * Function that converts field value to value masked with {@link #MASK_VALUE} - */ - static final Function VALUE_TO_MASKED_VALUE = value -> value.substring(0, 1) - .concat(MASK_VALUE.repeat(value.length() - 1)); - - /** - * User who is allowed to see documents on all indices where value of the {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_STRING}. - */ - static final TestSecurityConfig.User ALL_INDICES_STRING_ARTIST_READER = new TestSecurityConfig.User("string_artist_reader") - .roles( - new TestSecurityConfig.Role("string_artist_reader") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_STRING)) - .on("*") - ); - - /** - * User who is allowed to see documents on index: - *
    - *
  • {@link #FIRST_INDEX_NAME} where value of the {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_TWINS}
  • - *
  • {@link #SECOND_INDEX_NAME} where value of the {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_FIRST}
  • - *
- */ - static final TestSecurityConfig.User TWINS_FIRST_ARTIST_READER = new TestSecurityConfig.User("twins_first_artist_reader") - .roles( - new TestSecurityConfig.Role("twins_artist_reader") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_TWINS)) - .on(FIRST_INDEX_NAME), - new TestSecurityConfig.Role("first_artist_reader") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_FIRST)) - .on(SECOND_INDEX_NAME) - ); - - /** - * User who is allowed to see documents on all indices where value of the {@link Song#FIELD_STARS} is less than zero. - */ - static final TestSecurityConfig.User ALL_INDICES_STARS_LESS_THAN_ZERO_READER = new TestSecurityConfig.User("stars_less_than_zero_reader") - .roles( - new TestSecurityConfig.Role("stars_less_than_zero_reader") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .dls(String.format("{\"range\":{\"%s\":{\"lt\":%d}}}", FIELD_STARS, 0)) - .on("*") - ); - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(false) - .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .users( - ADMIN_USER, - ALL_INDICES_MASKED_TITLE_ARTIST_READER, MASKED_ARTIST_LYRICS_READER, - ALL_INDICES_STRING_ARTIST_READER, ALL_INDICES_STARS_LESS_THAN_ZERO_READER, TWINS_FIRST_ARTIST_READER - ) - .build(); - - /** - * Function that returns id assigned to song with title equal to given title or throws {@link RuntimeException} - * when no song matches. - */ - static final BiFunction, String, String> FIND_ID_OF_SONG_WITH_TITLE = (map, title) -> map.entrySet() - .stream().filter(entry -> title.equals(entry.getValue().getTitle())) - .findAny() - .map(Map.Entry::getKey) - .orElseThrow(() -> new RuntimeException("Cannot find id of song with title: " + title)); - - /** - * Function that returns id assigned to song with artist equal to given artist or throws {@link RuntimeException} - * when no song matches. - */ - static final BiFunction, String, String> FIND_ID_OF_SONG_WITH_ARTIST = (map, artist) -> map.entrySet() - .stream().filter(entry -> artist.equals(entry.getValue().getArtist())) - .findAny() - .map(Map.Entry::getKey) - .orElseThrow(() -> new RuntimeException("Cannot find id of song with artist: " + artist)); - - static final TreeMap FIRST_INDEX_SONGS_BY_ID = new TreeMap<>() {{ - put(FIRST_INDEX_ID_SONG_1, SONGS[0]); - put(FIRST_INDEX_ID_SONG_2, SONGS[1]); - put(FIRST_INDEX_ID_SONG_3, SONGS[2]); - put(FIRST_INDEX_ID_SONG_4, SONGS[3]); - }}; - - static final TreeMap SECOND_INDEX_SONGS_BY_ID = new TreeMap<>() {{ - put(SECOND_INDEX_ID_SONG_1, SONGS[3]); - put(SECOND_INDEX_ID_SONG_2, SONGS[2]); - put(SECOND_INDEX_ID_SONG_3, SONGS[1]); - put(SECOND_INDEX_ID_SONG_4, SONGS[0]); - }}; - - @BeforeClass - public static void createTestData() { - try(Client client = cluster.getInternalNodeClient()){ - FIRST_INDEX_SONGS_BY_ID.forEach((id, song) -> { - client.prepareIndex(FIRST_INDEX_NAME).setId(id).setRefreshPolicy(IMMEDIATE).setSource(song.asMap()).get(); - }); - - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD) - .indices(FIRST_INDEX_NAME) - .alias(FIRST_INDEX_ALIAS) - )).actionGet(); - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD) - .index(FIRST_INDEX_NAME) - .alias(FIRST_INDEX_ALIAS_FILTERED_BY_NEXT_SONG_TITLE) - .filter(QueryBuilders.queryStringQuery(QUERY_TITLE_NEXT_SONG)) - )).actionGet(); - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD) - .index(FIRST_INDEX_NAME) - .alias(FIRST_INDEX_ALIAS_FILTERED_BY_TWINS_ARTIST) - .filter(QueryBuilders.queryStringQuery(String.format("%s:%s", FIELD_ARTIST, ARTIST_TWINS))) - )).actionGet(); - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD) - .index(FIRST_INDEX_NAME) - .alias(FIRST_INDEX_ALIAS_FILTERED_BY_FIRST_ARTIST) - .filter(QueryBuilders.queryStringQuery(String.format("%s:%s", FIELD_ARTIST, ARTIST_FIRST))) - )).actionGet(); - - SECOND_INDEX_SONGS_BY_ID.forEach((id, song) -> { - client.prepareIndex(SECOND_INDEX_NAME).setId(id).setRefreshPolicy(IMMEDIATE).setSource(song.asMap()).get(); - }); - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD) - .indices(SECOND_INDEX_NAME) - .alias(SECOND_INDEX_ALIAS) - )).actionGet(); - } - } - - @Test - public void flsEnabledFieldsAreHiddenForNormalUsers() throws IOException { - String indexName = "fls_index"; - String indexAlias = "fls_index_alias"; - String indexFilteredAlias = "fls_index_filtered_alias"; - TestSecurityConfig.Role userRole = new TestSecurityConfig.Role("fls_exclude_stars_reader") - .clusterPermissions("cluster_composite_ops_ro") - .indexPermissions("read") - .fls("~".concat(FIELD_STARS)) - .on("*"); - TestSecurityConfig.User user = createUserWithRole("fls_user", userRole); - List docIds = createIndexWithDocs(indexName, SONGS[0], SONGS[1]); - addAliasToIndex(indexName, indexAlias); - addAliasToIndex(indexName, indexFilteredAlias, QueryBuilders.queryStringQuery(String.format("%s:%s", FIELD_ARTIST, SONGS[0].getArtist()))); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(user)) { - //search - SearchResponse searchResponse = restHighLevelClient.search(new SearchRequest(indexName), DEFAULT); - - assertSearchHitsDoNotContainField(searchResponse, FIELD_STARS); - - //search with index pattern - searchResponse = restHighLevelClient.search(new SearchRequest("*".concat(indexName)), DEFAULT); - - assertSearchHitsDoNotContainField(searchResponse, FIELD_STARS); - - //search via alias - searchResponse = restHighLevelClient.search(new SearchRequest(indexAlias), DEFAULT); - - assertSearchHitsDoNotContainField(searchResponse, FIELD_STARS); - - //search via filtered alias - searchResponse = restHighLevelClient.search(new SearchRequest(indexFilteredAlias), DEFAULT); - - assertSearchHitsDoNotContainField(searchResponse, FIELD_STARS); - - //search via all indices alias - searchResponse = restHighLevelClient.search(new SearchRequest(ALL_INDICES_ALIAS), DEFAULT); - - assertSearchHitsDoNotContainField(searchResponse, FIELD_STARS); - - //scroll - searchResponse = restHighLevelClient.search(searchRequestWithScroll(indexName, 1), DEFAULT); - - assertSearchHitsDoNotContainField(searchResponse, FIELD_STARS); - - SearchScrollRequest scrollRequest = getSearchScrollRequest(searchResponse); - SearchResponse scrollResponse = restHighLevelClient.scroll(scrollRequest, DEFAULT); - - assertSearchHitsDoNotContainField(scrollResponse, FIELD_STARS); - - //aggregate data and compute avg - String aggregationName = "averageStars"; - searchResponse = restHighLevelClient.search(averageAggregationRequest(indexName, aggregationName, FIELD_STARS), DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); - Aggregation actualAggregation = searchResponse.getAggregations().get(aggregationName); - assertThat(actualAggregation, instanceOf(ParsedAvg.class)); - assertThat(((ParsedAvg) actualAggregation).getValue(), is(Double.POSITIVE_INFINITY)); //user cannot see the STARS field - - //get document - GetResponse getResponse = restHighLevelClient.get(new GetRequest(indexName, docIds.get(0)), DEFAULT); - - assertThat(getResponse, documentDoesNotContainField(FIELD_STARS)); - - //multi get - for (String index: List.of(indexName, indexAlias)) { - MultiGetRequest multiGetRequest = new MultiGetRequest(); - docIds.forEach(id -> multiGetRequest.add(new MultiGetRequest.Item(index, id))); - - MultiGetResponse multiGetResponse = restHighLevelClient.mget(multiGetRequest, DEFAULT); - - List getResponses = Arrays.stream(multiGetResponse.getResponses()) - .map(MultiGetItemResponse::getResponse) - .collect(Collectors.toList()); - assertThat(getResponses, everyItem(documentDoesNotContainField(FIELD_STARS))); - } - - //multi search - for (String index: List.of(indexName, indexAlias)) { - MultiSearchRequest multiSearchRequest = new MultiSearchRequest(); - docIds.forEach(id -> multiSearchRequest.add(queryByIdsRequest(index, id))); - MultiSearchResponse multiSearchResponse = restHighLevelClient.msearch(multiSearchRequest, DEFAULT); - - assertThat(multiSearchResponse, isSuccessfulMultiSearchResponse()); - List itemResponses = List.of(multiSearchResponse.getResponses()); - itemResponses.forEach(item -> assertSearchHitsDoNotContainField(item.getResponse(), FIELD_STARS)); - } - - //field capabilities - FieldCapabilitiesResponse fieldCapsResponse = restHighLevelClient.fieldCaps( - new FieldCapabilitiesRequest().indices(indexName).fields(FIELD_TITLE, FIELD_STARS), DEFAULT - ); - assertThat(fieldCapsResponse.getField(FIELD_STARS), nullValue()); - } - } - - private static List createIndexWithDocs(String indexName, Song... songs) { - try(Client client = cluster.getInternalNodeClient()){ - return Stream.of(songs).map(song -> { - IndexResponse response = client.index( - new IndexRequest(indexName).setRefreshPolicy(IMMEDIATE).source(song.asMap()) - ).actionGet(); - return response.getId(); - }).collect(Collectors.toList()); - } - } - - private static void addAliasToIndex(String indexName, String alias) { - addAliasToIndex(indexName, alias, QueryBuilders.matchAllQuery()); - } - - private static void addAliasToIndex(String indexName, String alias, QueryBuilder filterQuery) { - try(Client client = cluster.getInternalNodeClient()){ - client.admin().indices() - .aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD) - .indices(indexName) - .alias(alias) - .filter(filterQuery) - )).actionGet(); - } - } - - private static TestSecurityConfig.User createUserWithRole(String userName, TestSecurityConfig.Role role) { - TestSecurityConfig.User user = new TestSecurityConfig.User(userName); - try(TestRestClient client = cluster.getRestClient(ADMIN_USER)) { - client.createRole(role.getName(), role).assertStatusCode(201); - client.createUser(user.getName(), user).assertStatusCode(201); - client.assignRoleToUser(user.getName(), role.getName()).assertStatusCode(200); - } - return user; - } - - private static void assertSearchHitsDoNotContainField(SearchResponse response, String excludedField) { - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response.getHits().getHits().length, greaterThan(0)); - IntStream.range(0, response.getHits().getHits().length).boxed() - .forEach(index -> assertThat(response, searchHitDoesNotContainField(index, excludedField))); - } - - @Test - public void searchForDocuments() throws IOException { - //FIELD MASKING - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { - String songId = FIRST_INDEX_ID_SONG_1; - Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); - - SearchRequest searchRequest = queryByIdsRequest(FIRST_INDEX_NAME, songId); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, songId)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); - - songId = SECOND_INDEX_ID_SONG_2; - song = SECOND_INDEX_SONGS_BY_ID.get(songId); - - searchRequest = queryByIdsRequest(SECOND_INDEX_NAME, songId); - searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, SECOND_INDEX_NAME, songId)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, song.getArtist())); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); - } - } - - @Test - public void searchForDocumentsWithIndexPattern() throws IOException { - //FIELD MASKING - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { - String songId = FIRST_INDEX_ID_SONG_2; - Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); - - SearchRequest searchRequest = queryByIdsRequest("*".concat(FIRST_INDEX_NAME), songId); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, songId)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); - - - songId = SECOND_INDEX_ID_SONG_3; - song = SECOND_INDEX_SONGS_BY_ID.get(songId); - - searchRequest = queryByIdsRequest("*".concat(SECOND_INDEX_NAME), songId); - searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, SECOND_INDEX_NAME, songId)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, song.getArtist())); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); - } - } - - @Test - public void searchForDocumentsViaAlias() throws IOException { - //FIELD MASKING - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { - String songId = FIRST_INDEX_ID_SONG_3; - Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); - - SearchRequest searchRequest = queryByIdsRequest(FIRST_INDEX_ALIAS, songId); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, songId)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); - - songId = SECOND_INDEX_ID_SONG_4; - song = SECOND_INDEX_SONGS_BY_ID.get(songId); - - searchRequest = queryByIdsRequest("*".concat(SECOND_INDEX_ALIAS), songId); - searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, SECOND_INDEX_NAME, songId)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, song.getArtist())); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); - } - } - - @Test - public void searchForDocumentsViaFilteredAlias() throws IOException { - //FIELD MASKING - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { - String songId = FIND_ID_OF_SONG_WITH_TITLE.apply(FIRST_INDEX_SONGS_BY_ID, TITLE_NEXT_SONG); - Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); - - SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_ALIAS_FILTERED_BY_NEXT_SONG_TITLE); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, songId)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); - } - } - - @Test - public void searchForDocumentsViaAllIndicesAlias() throws IOException { - //FIELD MASKING - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ALL_INDICES_MASKED_TITLE_ARTIST_READER)) { - String songId = FIRST_INDEX_ID_SONG_4; - Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); - - SearchRequest searchRequest = queryByIdsRequest(ALL_INDICES_ALIAS, songId); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, songId)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, VALUE_TO_MASKED_VALUE.apply(song.getTitle()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, song.getLyrics())); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); - - - songId = SECOND_INDEX_ID_SONG_1; - song = SECOND_INDEX_SONGS_BY_ID.get(songId); - - searchRequest = queryByIdsRequest(ALL_INDICES_ALIAS, songId); - searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, SECOND_INDEX_NAME, songId)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, VALUE_TO_MASKED_VALUE.apply(song.getTitle()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, song.getLyrics())); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); - } - } - - @Test - public void scrollOverSearchResults() throws IOException { - //FIELD MASKING - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { - String songId = FIRST_INDEX_SONGS_BY_ID.firstKey(); - Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); - - SearchRequest searchRequest = searchRequestWithScroll(FIRST_INDEX_NAME, 1); - searchRequest.source().sort("_id", SortOrder.ASC); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, containNotEmptyScrollingId()); - - SearchScrollRequest scrollRequest = getSearchScrollRequest(searchResponse); - - SearchResponse scrollResponse = restHighLevelClient.scroll(scrollRequest, DEFAULT); - assertThat(scrollResponse, isSuccessfulSearchResponse()); - assertThat(scrollResponse, containNotEmptyScrollingId()); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, songId)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); - } - } - - @Test - public void aggregateDataAndComputeAverage() throws IOException { - //FIELD MASKING - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { - String aggregationName = "averageStars"; - Double expectedValue = FIRST_INDEX_SONGS_BY_ID.values() - .stream() - .mapToDouble(Song::getStars) - .average().orElseThrow(() -> new RuntimeException("Cannot compute average stars - list of docs is empty")); - SearchRequest searchRequest = averageAggregationRequest(FIRST_INDEX_NAME, aggregationName, FIELD_STARS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); - Aggregation actualAggregation = searchResponse.getAggregations().get(aggregationName); - assertThat(actualAggregation, instanceOf(ParsedAvg.class)); - assertThat(((ParsedAvg) actualAggregation).getValue(), is(expectedValue)); - } - } - - @Test - public void getDocument() throws IOException { - //FIELD MASKING - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { - String songId = FIRST_INDEX_ID_SONG_4; - Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); - GetResponse response = restHighLevelClient.get(new GetRequest(FIRST_INDEX_NAME, songId), DEFAULT); - - assertThat(response, containDocument(FIRST_INDEX_NAME, songId)); - assertThat(response, documentContainField(FIELD_TITLE, song.getTitle())); - assertThat(response, documentContainField(FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); - assertThat(response, documentContainField(FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); - assertThat(response, documentContainField(FIELD_STARS, song.getStars())); - - songId = SECOND_INDEX_ID_SONG_1; - song = SECOND_INDEX_SONGS_BY_ID.get(songId); - response = restHighLevelClient.get(new GetRequest(SECOND_INDEX_NAME, songId), DEFAULT); - - assertThat(response, containDocument(SECOND_INDEX_NAME, songId)); - assertThat(response, documentContainField(FIELD_TITLE, song.getTitle())); - assertThat(response, documentContainField(FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); - assertThat(response, documentContainField(FIELD_ARTIST, song.getArtist())); - assertThat(response, documentContainField(FIELD_STARS, song.getStars())); - } - } - - @Test - public void multiGetDocuments() throws IOException { - //FIELD MASKING - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { - List> indicesToCheck = List.of( - List.of(FIRST_INDEX_NAME, SECOND_INDEX_NAME), - List.of(FIRST_INDEX_ALIAS, SECOND_INDEX_ALIAS) - ); - String firstSongId = FIRST_INDEX_ID_SONG_1; - Song firstSong = FIRST_INDEX_SONGS_BY_ID.get(firstSongId); - String secondSongId = SECOND_INDEX_ID_SONG_2; - Song secondSong = SECOND_INDEX_SONGS_BY_ID.get(secondSongId); - - for (List indices : indicesToCheck) { - MultiGetRequest request = new MultiGetRequest(); - request.add(new MultiGetRequest.Item(indices.get(0), firstSongId)); - request.add(new MultiGetRequest.Item(indices.get(1), secondSongId)); - MultiGetResponse response = restHighLevelClient.mget(request, DEFAULT); - - assertThat(response, isSuccessfulMultiGetResponse()); - assertThat(response, numberOfGetItemResponsesIsEqualTo(2)); - - MultiGetItemResponse[] responses = response.getResponses(); - assertThat(responses[0].getResponse(), allOf( - containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1), - documentContainField(FIELD_TITLE, firstSong.getTitle()), - documentContainField(FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(firstSong.getLyrics())), - documentContainField(FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(firstSong.getArtist())), - documentContainField(FIELD_STARS, firstSong.getStars()) - )); - assertThat(responses[1].getResponse(), allOf( - containDocument(SECOND_INDEX_NAME, secondSongId), - documentContainField(FIELD_TITLE, secondSong.getTitle()), - documentContainField(FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(secondSong.getLyrics())), - documentContainField(FIELD_ARTIST, secondSong.getArtist()), - documentContainField(FIELD_STARS, secondSong.getStars()) - )); - } - } - } - - @Test - public void multiSearchDocuments() throws IOException { - //FIELD MASKING - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { - List> indicesToCheck = List.of( - List.of(FIRST_INDEX_NAME, SECOND_INDEX_NAME), - List.of(FIRST_INDEX_ALIAS, SECOND_INDEX_ALIAS) - ); - String firstSongId = FIRST_INDEX_ID_SONG_3; - Song firstSong = FIRST_INDEX_SONGS_BY_ID.get(firstSongId); - String secondSongId = SECOND_INDEX_ID_SONG_4; - Song secondSong = SECOND_INDEX_SONGS_BY_ID.get(secondSongId); - - for (List indices : indicesToCheck) { - MultiSearchRequest request = new MultiSearchRequest(); - request.add(queryByIdsRequest(indices.get(0), firstSongId)); - request.add(queryByIdsRequest(indices.get(1), secondSongId)); - MultiSearchResponse response = restHighLevelClient.msearch(request, DEFAULT); - - assertThat(response, isSuccessfulMultiSearchResponse()); - assertThat(response, numberOfSearchItemResponsesIsEqualTo(2)); - - MultiSearchResponse.Item[] responses = response.getResponses(); - - assertThat(responses[0].getResponse(), allOf( - searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, firstSongId), - searchHitContainsFieldWithValue(0, FIELD_TITLE, firstSong.getTitle()), - searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(firstSong.getLyrics())), - searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(firstSong.getArtist())), - searchHitContainsFieldWithValue(0, FIELD_STARS, firstSong.getStars()) - )); - assertThat(responses[1].getResponse(), allOf( - searchHitsContainDocumentWithId(0, SECOND_INDEX_NAME, secondSongId), - searchHitContainsFieldWithValue(0, FIELD_TITLE, secondSong.getTitle()), - searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(secondSong.getLyrics())), - searchHitContainsFieldWithValue(0, FIELD_ARTIST, secondSong.getArtist()), - searchHitContainsFieldWithValue(0, FIELD_STARS, secondSong.getStars()) - )); - } - } - } - - @Test - public void getFieldCapabilities() throws IOException { - //FIELD MASKING - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { - FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(FIRST_INDEX_NAME).fields(FIELD_ARTIST, FIELD_TITLE, FIELD_LYRICS); - FieldCapabilitiesResponse response = restHighLevelClient.fieldCaps(request, DEFAULT); - - assertThat(response, containsExactlyIndices(FIRST_INDEX_NAME)); - assertThat(response, numberOfFieldsIsEqualTo(3)); - assertThat(response, containsFieldWithNameAndType(FIELD_ARTIST, "text")); - assertThat(response, containsFieldWithNameAndType(FIELD_TITLE, "text")); - assertThat(response, containsFieldWithNameAndType(FIELD_LYRICS, "text")); - } - } + static final String FIRST_INDEX_ID_SONG_1 = "INDEX_1_S1"; + static final String FIRST_INDEX_ID_SONG_2 = "INDEX_1_S2"; + static final String FIRST_INDEX_ID_SONG_3 = "INDEX_1_S3"; + static final String FIRST_INDEX_ID_SONG_4 = "INDEX_1_S4"; + static final String SECOND_INDEX_ID_SONG_1 = "INDEX_2_S1"; + static final String SECOND_INDEX_ID_SONG_2 = "INDEX_2_S2"; + static final String SECOND_INDEX_ID_SONG_3 = "INDEX_2_S3"; + static final String SECOND_INDEX_ID_SONG_4 = "INDEX_2_S4"; + + static final String INDEX_NAME_SUFFIX = "-test-index"; + static final String FIRST_INDEX_NAME = "first".concat(INDEX_NAME_SUFFIX); + static final String SECOND_INDEX_NAME = "second".concat(INDEX_NAME_SUFFIX); + static final String FIRST_INDEX_ALIAS = FIRST_INDEX_NAME.concat("-alias"); + static final String SECOND_INDEX_ALIAS = SECOND_INDEX_NAME.concat("-alias"); + static final String FIRST_INDEX_ALIAS_FILTERED_BY_NEXT_SONG_TITLE = FIRST_INDEX_NAME.concat("-filtered-by-next-song-title"); + static final String FIRST_INDEX_ALIAS_FILTERED_BY_TWINS_ARTIST = FIRST_INDEX_NAME.concat("-filtered-by-twins-artist"); + static final String FIRST_INDEX_ALIAS_FILTERED_BY_FIRST_ARTIST = FIRST_INDEX_NAME.concat("-filtered-by-first-artist"); + static final String ALL_INDICES_ALIAS = "_all"; + + static final String MASK_VALUE = "*"; + + static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + /** + * User who is allowed to see all fields on all indices. Values of the title and artist fields should be masked. + */ + static final TestSecurityConfig.User ALL_INDICES_MASKED_TITLE_ARTIST_READER = new TestSecurityConfig.User("masked_artist_title_reader") + .roles( + new TestSecurityConfig.Role("masked_artist_title_reader").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .maskedFields( + FIELD_TITLE.concat("::/(?<=.{1})./::").concat(MASK_VALUE), + FIELD_ARTIST.concat("::/(?<=.{1})./::").concat(MASK_VALUE) + ) + .on("*") + ); + + /** + * User who is allowed to see all fields on indices {@link #FIRST_INDEX_NAME} and {@link #SECOND_INDEX_NAME}. + *
    + *
  • values of the artist and lyrics fields should be masked on index {@link #FIRST_INDEX_NAME}
  • + *
  • values of the lyrics field should be masked on index {@link #SECOND_INDEX_NAME}
  • + *
+ */ + static final TestSecurityConfig.User MASKED_ARTIST_LYRICS_READER = new TestSecurityConfig.User("masked_title_artist_lyrics_reader") + .roles( + new TestSecurityConfig.Role("masked_title_artist_lyrics_reader").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .maskedFields( + FIELD_ARTIST.concat("::/(?<=.{1})./::").concat(MASK_VALUE), + FIELD_LYRICS.concat("::/(?<=.{1})./::").concat(MASK_VALUE) + ) + .on(FIRST_INDEX_NAME), + new TestSecurityConfig.Role("masked_lyrics_reader").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .maskedFields(FIELD_LYRICS.concat("::/(?<=.{1})./::").concat(MASK_VALUE)) + .on(SECOND_INDEX_NAME) + ); + + /** + * Function that converts field value to value masked with {@link #MASK_VALUE} + */ + static final Function VALUE_TO_MASKED_VALUE = value -> value.substring(0, 1) + .concat(MASK_VALUE.repeat(value.length() - 1)); + + /** + * User who is allowed to see documents on all indices where value of the {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_STRING}. + */ + static final TestSecurityConfig.User ALL_INDICES_STRING_ARTIST_READER = new TestSecurityConfig.User("string_artist_reader").roles( + new TestSecurityConfig.Role("string_artist_reader").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_STRING)) + .on("*") + ); + + /** + * User who is allowed to see documents on index: + *
    + *
  • {@link #FIRST_INDEX_NAME} where value of the {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_TWINS}
  • + *
  • {@link #SECOND_INDEX_NAME} where value of the {@link Song#FIELD_ARTIST} field matches {@link Song#ARTIST_FIRST}
  • + *
+ */ + static final TestSecurityConfig.User TWINS_FIRST_ARTIST_READER = new TestSecurityConfig.User("twins_first_artist_reader").roles( + new TestSecurityConfig.Role("twins_artist_reader").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_TWINS)) + .on(FIRST_INDEX_NAME), + new TestSecurityConfig.Role("first_artist_reader").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .dls(String.format("{\"match\":{\"%s\":\"%s\"}}", FIELD_ARTIST, ARTIST_FIRST)) + .on(SECOND_INDEX_NAME) + ); + + /** + * User who is allowed to see documents on all indices where value of the {@link Song#FIELD_STARS} is less than zero. + */ + static final TestSecurityConfig.User ALL_INDICES_STARS_LESS_THAN_ZERO_READER = new TestSecurityConfig.User( + "stars_less_than_zero_reader" + ).roles( + new TestSecurityConfig.Role("stars_less_than_zero_reader").clusterPermissions("cluster_composite_ops_ro") + .indexPermissions("read") + .dls(String.format("{\"range\":{\"%s\":{\"lt\":%d}}}", FIELD_STARS, 0)) + .on("*") + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .anonymousAuth(false) + .nodeSettings( + Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName())) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users( + ADMIN_USER, + ALL_INDICES_MASKED_TITLE_ARTIST_READER, + MASKED_ARTIST_LYRICS_READER, + ALL_INDICES_STRING_ARTIST_READER, + ALL_INDICES_STARS_LESS_THAN_ZERO_READER, + TWINS_FIRST_ARTIST_READER + ) + .build(); + + /** + * Function that returns id assigned to song with title equal to given title or throws {@link RuntimeException} + * when no song matches. + */ + static final BiFunction, String, String> FIND_ID_OF_SONG_WITH_TITLE = (map, title) -> map.entrySet() + .stream() + .filter(entry -> title.equals(entry.getValue().getTitle())) + .findAny() + .map(Map.Entry::getKey) + .orElseThrow(() -> new RuntimeException("Cannot find id of song with title: " + title)); + + /** + * Function that returns id assigned to song with artist equal to given artist or throws {@link RuntimeException} + * when no song matches. + */ + static final BiFunction, String, String> FIND_ID_OF_SONG_WITH_ARTIST = (map, artist) -> map.entrySet() + .stream() + .filter(entry -> artist.equals(entry.getValue().getArtist())) + .findAny() + .map(Map.Entry::getKey) + .orElseThrow(() -> new RuntimeException("Cannot find id of song with artist: " + artist)); + + static final TreeMap FIRST_INDEX_SONGS_BY_ID = new TreeMap<>() { + { + put(FIRST_INDEX_ID_SONG_1, SONGS[0]); + put(FIRST_INDEX_ID_SONG_2, SONGS[1]); + put(FIRST_INDEX_ID_SONG_3, SONGS[2]); + put(FIRST_INDEX_ID_SONG_4, SONGS[3]); + } + }; + + static final TreeMap SECOND_INDEX_SONGS_BY_ID = new TreeMap<>() { + { + put(SECOND_INDEX_ID_SONG_1, SONGS[3]); + put(SECOND_INDEX_ID_SONG_2, SONGS[2]); + put(SECOND_INDEX_ID_SONG_3, SONGS[1]); + put(SECOND_INDEX_ID_SONG_4, SONGS[0]); + } + }; + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + FIRST_INDEX_SONGS_BY_ID.forEach((id, song) -> { + client.prepareIndex(FIRST_INDEX_NAME).setId(id).setRefreshPolicy(IMMEDIATE).setSource(song.asMap()).get(); + }); + + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).indices(FIRST_INDEX_NAME).alias(FIRST_INDEX_ALIAS) + ) + ) + .actionGet(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).index(FIRST_INDEX_NAME) + .alias(FIRST_INDEX_ALIAS_FILTERED_BY_NEXT_SONG_TITLE) + .filter(QueryBuilders.queryStringQuery(QUERY_TITLE_NEXT_SONG)) + ) + ) + .actionGet(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).index(FIRST_INDEX_NAME) + .alias(FIRST_INDEX_ALIAS_FILTERED_BY_TWINS_ARTIST) + .filter(QueryBuilders.queryStringQuery(String.format("%s:%s", FIELD_ARTIST, ARTIST_TWINS))) + ) + ) + .actionGet(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).index(FIRST_INDEX_NAME) + .alias(FIRST_INDEX_ALIAS_FILTERED_BY_FIRST_ARTIST) + .filter(QueryBuilders.queryStringQuery(String.format("%s:%s", FIELD_ARTIST, ARTIST_FIRST))) + ) + ) + .actionGet(); + + SECOND_INDEX_SONGS_BY_ID.forEach((id, song) -> { + client.prepareIndex(SECOND_INDEX_NAME).setId(id).setRefreshPolicy(IMMEDIATE).setSource(song.asMap()).get(); + }); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).indices(SECOND_INDEX_NAME).alias(SECOND_INDEX_ALIAS) + ) + ) + .actionGet(); + } + } + + @Test + public void flsEnabledFieldsAreHiddenForNormalUsers() throws IOException { + String indexName = "fls_index"; + String indexAlias = "fls_index_alias"; + String indexFilteredAlias = "fls_index_filtered_alias"; + TestSecurityConfig.Role userRole = new TestSecurityConfig.Role("fls_exclude_stars_reader").clusterPermissions( + "cluster_composite_ops_ro" + ).indexPermissions("read").fls("~".concat(FIELD_STARS)).on("*"); + TestSecurityConfig.User user = createUserWithRole("fls_user", userRole); + List docIds = createIndexWithDocs(indexName, SONGS[0], SONGS[1]); + addAliasToIndex(indexName, indexAlias); + addAliasToIndex( + indexName, + indexFilteredAlias, + QueryBuilders.queryStringQuery(String.format("%s:%s", FIELD_ARTIST, SONGS[0].getArtist())) + ); + + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(user)) { + // search + SearchResponse searchResponse = restHighLevelClient.search(new SearchRequest(indexName), DEFAULT); + + assertSearchHitsDoNotContainField(searchResponse, FIELD_STARS); + + // search with index pattern + searchResponse = restHighLevelClient.search(new SearchRequest("*".concat(indexName)), DEFAULT); + + assertSearchHitsDoNotContainField(searchResponse, FIELD_STARS); + + // search via alias + searchResponse = restHighLevelClient.search(new SearchRequest(indexAlias), DEFAULT); + + assertSearchHitsDoNotContainField(searchResponse, FIELD_STARS); + + // search via filtered alias + searchResponse = restHighLevelClient.search(new SearchRequest(indexFilteredAlias), DEFAULT); + + assertSearchHitsDoNotContainField(searchResponse, FIELD_STARS); + + // search via all indices alias + searchResponse = restHighLevelClient.search(new SearchRequest(ALL_INDICES_ALIAS), DEFAULT); + + assertSearchHitsDoNotContainField(searchResponse, FIELD_STARS); + + // scroll + searchResponse = restHighLevelClient.search(searchRequestWithScroll(indexName, 1), DEFAULT); + + assertSearchHitsDoNotContainField(searchResponse, FIELD_STARS); + + SearchScrollRequest scrollRequest = getSearchScrollRequest(searchResponse); + SearchResponse scrollResponse = restHighLevelClient.scroll(scrollRequest, DEFAULT); + + assertSearchHitsDoNotContainField(scrollResponse, FIELD_STARS); + + // aggregate data and compute avg + String aggregationName = "averageStars"; + searchResponse = restHighLevelClient.search(averageAggregationRequest(indexName, aggregationName, FIELD_STARS), DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); + Aggregation actualAggregation = searchResponse.getAggregations().get(aggregationName); + assertThat(actualAggregation, instanceOf(ParsedAvg.class)); + assertThat(((ParsedAvg) actualAggregation).getValue(), is(Double.POSITIVE_INFINITY)); // user cannot see the STARS field + + // get document + GetResponse getResponse = restHighLevelClient.get(new GetRequest(indexName, docIds.get(0)), DEFAULT); + + assertThat(getResponse, documentDoesNotContainField(FIELD_STARS)); + + // multi get + for (String index : List.of(indexName, indexAlias)) { + MultiGetRequest multiGetRequest = new MultiGetRequest(); + docIds.forEach(id -> multiGetRequest.add(new MultiGetRequest.Item(index, id))); + + MultiGetResponse multiGetResponse = restHighLevelClient.mget(multiGetRequest, DEFAULT); + + List getResponses = Arrays.stream(multiGetResponse.getResponses()) + .map(MultiGetItemResponse::getResponse) + .collect(Collectors.toList()); + assertThat(getResponses, everyItem(documentDoesNotContainField(FIELD_STARS))); + } + + // multi search + for (String index : List.of(indexName, indexAlias)) { + MultiSearchRequest multiSearchRequest = new MultiSearchRequest(); + docIds.forEach(id -> multiSearchRequest.add(queryByIdsRequest(index, id))); + MultiSearchResponse multiSearchResponse = restHighLevelClient.msearch(multiSearchRequest, DEFAULT); + + assertThat(multiSearchResponse, isSuccessfulMultiSearchResponse()); + List itemResponses = List.of(multiSearchResponse.getResponses()); + itemResponses.forEach(item -> assertSearchHitsDoNotContainField(item.getResponse(), FIELD_STARS)); + } + + // field capabilities + FieldCapabilitiesResponse fieldCapsResponse = restHighLevelClient.fieldCaps( + new FieldCapabilitiesRequest().indices(indexName).fields(FIELD_TITLE, FIELD_STARS), + DEFAULT + ); + assertThat(fieldCapsResponse.getField(FIELD_STARS), nullValue()); + } + } + + private static List createIndexWithDocs(String indexName, Song... songs) { + try (Client client = cluster.getInternalNodeClient()) { + return Stream.of(songs).map(song -> { + IndexResponse response = client.index(new IndexRequest(indexName).setRefreshPolicy(IMMEDIATE).source(song.asMap())) + .actionGet(); + return response.getId(); + }).collect(Collectors.toList()); + } + } + + private static void addAliasToIndex(String indexName, String alias) { + addAliasToIndex(indexName, alias, QueryBuilders.matchAllQuery()); + } + + private static void addAliasToIndex(String indexName, String alias, QueryBuilder filterQuery) { + try (Client client = cluster.getInternalNodeClient()) { + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).indices(indexName).alias(alias).filter(filterQuery) + ) + ) + .actionGet(); + } + } + + private static TestSecurityConfig.User createUserWithRole(String userName, TestSecurityConfig.Role role) { + TestSecurityConfig.User user = new TestSecurityConfig.User(userName); + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + client.createRole(role.getName(), role).assertStatusCode(201); + client.createUser(user.getName(), user).assertStatusCode(201); + client.assignRoleToUser(user.getName(), role.getName()).assertStatusCode(200); + } + return user; + } + + private static void assertSearchHitsDoNotContainField(SearchResponse response, String excludedField) { + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response.getHits().getHits().length, greaterThan(0)); + IntStream.range(0, response.getHits().getHits().length) + .boxed() + .forEach(index -> assertThat(response, searchHitDoesNotContainField(index, excludedField))); + } + + @Test + public void searchForDocuments() throws IOException { + // FIELD MASKING + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { + String songId = FIRST_INDEX_ID_SONG_1; + Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); + + SearchRequest searchRequest = queryByIdsRequest(FIRST_INDEX_NAME, songId); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, songId)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); + + songId = SECOND_INDEX_ID_SONG_2; + song = SECOND_INDEX_SONGS_BY_ID.get(songId); + + searchRequest = queryByIdsRequest(SECOND_INDEX_NAME, songId); + searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SECOND_INDEX_NAME, songId)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, song.getArtist())); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); + } + } + + @Test + public void searchForDocumentsWithIndexPattern() throws IOException { + // FIELD MASKING + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { + String songId = FIRST_INDEX_ID_SONG_2; + Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); + + SearchRequest searchRequest = queryByIdsRequest("*".concat(FIRST_INDEX_NAME), songId); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, songId)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); + + songId = SECOND_INDEX_ID_SONG_3; + song = SECOND_INDEX_SONGS_BY_ID.get(songId); + + searchRequest = queryByIdsRequest("*".concat(SECOND_INDEX_NAME), songId); + searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SECOND_INDEX_NAME, songId)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, song.getArtist())); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); + } + } + + @Test + public void searchForDocumentsViaAlias() throws IOException { + // FIELD MASKING + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { + String songId = FIRST_INDEX_ID_SONG_3; + Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); + + SearchRequest searchRequest = queryByIdsRequest(FIRST_INDEX_ALIAS, songId); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, songId)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); + + songId = SECOND_INDEX_ID_SONG_4; + song = SECOND_INDEX_SONGS_BY_ID.get(songId); + + searchRequest = queryByIdsRequest("*".concat(SECOND_INDEX_ALIAS), songId); + searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SECOND_INDEX_NAME, songId)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, song.getArtist())); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); + } + } + + @Test + public void searchForDocumentsViaFilteredAlias() throws IOException { + // FIELD MASKING + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { + String songId = FIND_ID_OF_SONG_WITH_TITLE.apply(FIRST_INDEX_SONGS_BY_ID, TITLE_NEXT_SONG); + Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); + + SearchRequest searchRequest = new SearchRequest(FIRST_INDEX_ALIAS_FILTERED_BY_NEXT_SONG_TITLE); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, songId)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); + } + } + + @Test + public void searchForDocumentsViaAllIndicesAlias() throws IOException { + // FIELD MASKING + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ALL_INDICES_MASKED_TITLE_ARTIST_READER)) { + String songId = FIRST_INDEX_ID_SONG_4; + Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); + + SearchRequest searchRequest = queryByIdsRequest(ALL_INDICES_ALIAS, songId); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, songId)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, VALUE_TO_MASKED_VALUE.apply(song.getTitle()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, song.getLyrics())); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); + + songId = SECOND_INDEX_ID_SONG_1; + song = SECOND_INDEX_SONGS_BY_ID.get(songId); + + searchRequest = queryByIdsRequest(ALL_INDICES_ALIAS, songId); + searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SECOND_INDEX_NAME, songId)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, VALUE_TO_MASKED_VALUE.apply(song.getTitle()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, song.getLyrics())); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); + } + } + + @Test + public void scrollOverSearchResults() throws IOException { + // FIELD MASKING + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { + String songId = FIRST_INDEX_SONGS_BY_ID.firstKey(); + Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); + + SearchRequest searchRequest = searchRequestWithScroll(FIRST_INDEX_NAME, 1); + searchRequest.source().sort("_id", SortOrder.ASC); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containNotEmptyScrollingId()); + + SearchScrollRequest scrollRequest = getSearchScrollRequest(searchResponse); + + SearchResponse scrollResponse = restHighLevelClient.scroll(scrollRequest, DEFAULT); + assertThat(scrollResponse, isSuccessfulSearchResponse()); + assertThat(scrollResponse, containNotEmptyScrollingId()); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, songId)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, song.getTitle())); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_STARS, song.getStars())); + } + } + + @Test + public void aggregateDataAndComputeAverage() throws IOException { + // FIELD MASKING + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { + String aggregationName = "averageStars"; + Double expectedValue = FIRST_INDEX_SONGS_BY_ID.values() + .stream() + .mapToDouble(Song::getStars) + .average() + .orElseThrow(() -> new RuntimeException("Cannot compute average stars - list of docs is empty")); + SearchRequest searchRequest = averageAggregationRequest(FIRST_INDEX_NAME, aggregationName, FIELD_STARS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); + Aggregation actualAggregation = searchResponse.getAggregations().get(aggregationName); + assertThat(actualAggregation, instanceOf(ParsedAvg.class)); + assertThat(((ParsedAvg) actualAggregation).getValue(), is(expectedValue)); + } + } + + @Test + public void getDocument() throws IOException { + // FIELD MASKING + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { + String songId = FIRST_INDEX_ID_SONG_4; + Song song = FIRST_INDEX_SONGS_BY_ID.get(songId); + GetResponse response = restHighLevelClient.get(new GetRequest(FIRST_INDEX_NAME, songId), DEFAULT); + + assertThat(response, containDocument(FIRST_INDEX_NAME, songId)); + assertThat(response, documentContainField(FIELD_TITLE, song.getTitle())); + assertThat(response, documentContainField(FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); + assertThat(response, documentContainField(FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(song.getArtist()))); + assertThat(response, documentContainField(FIELD_STARS, song.getStars())); + + songId = SECOND_INDEX_ID_SONG_1; + song = SECOND_INDEX_SONGS_BY_ID.get(songId); + response = restHighLevelClient.get(new GetRequest(SECOND_INDEX_NAME, songId), DEFAULT); + + assertThat(response, containDocument(SECOND_INDEX_NAME, songId)); + assertThat(response, documentContainField(FIELD_TITLE, song.getTitle())); + assertThat(response, documentContainField(FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(song.getLyrics()))); + assertThat(response, documentContainField(FIELD_ARTIST, song.getArtist())); + assertThat(response, documentContainField(FIELD_STARS, song.getStars())); + } + } + + @Test + public void multiGetDocuments() throws IOException { + // FIELD MASKING + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { + List> indicesToCheck = List.of( + List.of(FIRST_INDEX_NAME, SECOND_INDEX_NAME), + List.of(FIRST_INDEX_ALIAS, SECOND_INDEX_ALIAS) + ); + String firstSongId = FIRST_INDEX_ID_SONG_1; + Song firstSong = FIRST_INDEX_SONGS_BY_ID.get(firstSongId); + String secondSongId = SECOND_INDEX_ID_SONG_2; + Song secondSong = SECOND_INDEX_SONGS_BY_ID.get(secondSongId); + + for (List indices : indicesToCheck) { + MultiGetRequest request = new MultiGetRequest(); + request.add(new MultiGetRequest.Item(indices.get(0), firstSongId)); + request.add(new MultiGetRequest.Item(indices.get(1), secondSongId)); + MultiGetResponse response = restHighLevelClient.mget(request, DEFAULT); + + assertThat(response, isSuccessfulMultiGetResponse()); + assertThat(response, numberOfGetItemResponsesIsEqualTo(2)); + + MultiGetItemResponse[] responses = response.getResponses(); + assertThat( + responses[0].getResponse(), + allOf( + containDocument(FIRST_INDEX_NAME, FIRST_INDEX_ID_SONG_1), + documentContainField(FIELD_TITLE, firstSong.getTitle()), + documentContainField(FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(firstSong.getLyrics())), + documentContainField(FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(firstSong.getArtist())), + documentContainField(FIELD_STARS, firstSong.getStars()) + ) + ); + assertThat( + responses[1].getResponse(), + allOf( + containDocument(SECOND_INDEX_NAME, secondSongId), + documentContainField(FIELD_TITLE, secondSong.getTitle()), + documentContainField(FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(secondSong.getLyrics())), + documentContainField(FIELD_ARTIST, secondSong.getArtist()), + documentContainField(FIELD_STARS, secondSong.getStars()) + ) + ); + } + } + } + + @Test + public void multiSearchDocuments() throws IOException { + // FIELD MASKING + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { + List> indicesToCheck = List.of( + List.of(FIRST_INDEX_NAME, SECOND_INDEX_NAME), + List.of(FIRST_INDEX_ALIAS, SECOND_INDEX_ALIAS) + ); + String firstSongId = FIRST_INDEX_ID_SONG_3; + Song firstSong = FIRST_INDEX_SONGS_BY_ID.get(firstSongId); + String secondSongId = SECOND_INDEX_ID_SONG_4; + Song secondSong = SECOND_INDEX_SONGS_BY_ID.get(secondSongId); + + for (List indices : indicesToCheck) { + MultiSearchRequest request = new MultiSearchRequest(); + request.add(queryByIdsRequest(indices.get(0), firstSongId)); + request.add(queryByIdsRequest(indices.get(1), secondSongId)); + MultiSearchResponse response = restHighLevelClient.msearch(request, DEFAULT); + + assertThat(response, isSuccessfulMultiSearchResponse()); + assertThat(response, numberOfSearchItemResponsesIsEqualTo(2)); + + MultiSearchResponse.Item[] responses = response.getResponses(); + + assertThat( + responses[0].getResponse(), + allOf( + searchHitsContainDocumentWithId(0, FIRST_INDEX_NAME, firstSongId), + searchHitContainsFieldWithValue(0, FIELD_TITLE, firstSong.getTitle()), + searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(firstSong.getLyrics())), + searchHitContainsFieldWithValue(0, FIELD_ARTIST, VALUE_TO_MASKED_VALUE.apply(firstSong.getArtist())), + searchHitContainsFieldWithValue(0, FIELD_STARS, firstSong.getStars()) + ) + ); + assertThat( + responses[1].getResponse(), + allOf( + searchHitsContainDocumentWithId(0, SECOND_INDEX_NAME, secondSongId), + searchHitContainsFieldWithValue(0, FIELD_TITLE, secondSong.getTitle()), + searchHitContainsFieldWithValue(0, FIELD_LYRICS, VALUE_TO_MASKED_VALUE.apply(secondSong.getLyrics())), + searchHitContainsFieldWithValue(0, FIELD_ARTIST, secondSong.getArtist()), + searchHitContainsFieldWithValue(0, FIELD_STARS, secondSong.getStars()) + ) + ); + } + } + } + + @Test + public void getFieldCapabilities() throws IOException { + // FIELD MASKING + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(MASKED_ARTIST_LYRICS_READER)) { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(FIRST_INDEX_NAME) + .fields(FIELD_ARTIST, FIELD_TITLE, FIELD_LYRICS); + FieldCapabilitiesResponse response = restHighLevelClient.fieldCaps(request, DEFAULT); + + assertThat(response, containsExactlyIndices(FIRST_INDEX_NAME)); + assertThat(response, numberOfFieldsIsEqualTo(3)); + assertThat(response, containsFieldWithNameAndType(FIELD_ARTIST, "text")); + assertThat(response, containsFieldWithNameAndType(FIELD_TITLE, "text")); + assertThat(response, containsFieldWithNameAndType(FIELD_LYRICS, "text")); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/IndexOperationsHelper.java b/src/integrationTest/java/org/opensearch/security/IndexOperationsHelper.java index c1001e5fc5..7482558c5b 100644 --- a/src/integrationTest/java/org/opensearch/security/IndexOperationsHelper.java +++ b/src/integrationTest/java/org/opensearch/security/IndexOperationsHelper.java @@ -28,39 +28,39 @@ public class IndexOperationsHelper { - public static void createIndex(LocalCluster cluster, String indexName) { - createIndex(cluster, indexName, Settings.EMPTY); - } + public static void createIndex(LocalCluster cluster, String indexName) { + createIndex(cluster, indexName, Settings.EMPTY); + } - public static void createIndex(LocalCluster cluster, String indexName, Settings settings) { - try(Client client = cluster.getInternalNodeClient()) { - CreateIndexResponse createIndexResponse = client.admin().indices() - .create(new CreateIndexRequest(indexName).settings(settings)) - .actionGet(); + public static void createIndex(LocalCluster cluster, String indexName, Settings settings) { + try (Client client = cluster.getInternalNodeClient()) { + CreateIndexResponse createIndexResponse = client.admin() + .indices() + .create(new CreateIndexRequest(indexName).settings(settings)) + .actionGet(); - assertThat(createIndexResponse.isAcknowledged(), is(true)); - assertThat(createIndexResponse.isShardsAcknowledged(), is(true)); - assertThat(cluster, indexExists(indexName)); - } - } + assertThat(createIndexResponse.isAcknowledged(), is(true)); + assertThat(createIndexResponse.isShardsAcknowledged(), is(true)); + assertThat(cluster, indexExists(indexName)); + } + } - public static void closeIndex(LocalCluster cluster, String indexName) { - try(Client client = cluster.getInternalNodeClient()) { - CloseIndexRequest closeIndexRequest = new CloseIndexRequest(indexName); - CloseIndexResponse response = client.admin().indices().close(closeIndexRequest).actionGet(); + public static void closeIndex(LocalCluster cluster, String indexName) { + try (Client client = cluster.getInternalNodeClient()) { + CloseIndexRequest closeIndexRequest = new CloseIndexRequest(indexName); + CloseIndexResponse response = client.admin().indices().close(closeIndexRequest).actionGet(); - assertThat(response.isAcknowledged(), is(true)); - assertThat(response.isShardsAcknowledged(), is(true)); - assertThat(cluster, indexStateIsEqualTo(indexName, IndexMetadata.State.CLOSE)); - } - } + assertThat(response.isAcknowledged(), is(true)); + assertThat(response.isShardsAcknowledged(), is(true)); + assertThat(cluster, indexStateIsEqualTo(indexName, IndexMetadata.State.CLOSE)); + } + } - public static void createMapping(LocalCluster cluster, String indexName, Map indexMapping) { - try(Client client = cluster.getInternalNodeClient()) { - var response = client.admin().indices() - .putMapping(new PutMappingRequest(indexName).source(indexMapping)).actionGet(); + public static void createMapping(LocalCluster cluster, String indexName, Map indexMapping) { + try (Client client = cluster.getInternalNodeClient()) { + var response = client.admin().indices().putMapping(new PutMappingRequest(indexName).source(indexMapping)).actionGet(); - assertThat(response.isAcknowledged(), is(true)); - } - } + assertThat(response.isAcknowledged(), is(true)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java index f1f9cdf3f8..bb16e0be1b 100644 --- a/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java +++ b/src/integrationTest/java/org/opensearch/security/IpBruteForceAttacksPreventionTests.java @@ -36,123 +36,131 @@ @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class IpBruteForceAttacksPreventionTests { - private static final User USER_1 = new User("simple-user-1").roles(ALL_ACCESS); - private static final User USER_2 = new User("simple-user-2").roles(ALL_ACCESS); - - public static final int ALLOWED_TRIES = 3; - public static final int TIME_WINDOW_SECONDS = 3; - - public static final String CLIENT_IP_2 = "127.0.0.2"; - public static final String CLIENT_IP_3 = "127.0.0.3"; - public static final String CLIENT_IP_4 = "127.0.0.4"; - public static final String CLIENT_IP_5 = "127.0.0.5"; - public static final String CLIENT_IP_6 = "127.0.0.6"; - public static final String CLIENT_IP_7 = "127.0.0.7"; - public static final String CLIENT_IP_8 = "127.0.0.8"; - public static final String CLIENT_IP_9 = "127.0.0.9"; - - private static final AuthFailureListeners listener = new AuthFailureListeners() - .addRateLimit(new RateLimiting("internal_authentication_backend_limiting").type("ip") - .allowedTries(ALLOWED_TRIES).timeWindowSeconds(TIME_WINDOW_SECONDS).blockExpirySeconds(2).maxBlockedClients(500) - .maxTrackedClients(500)); - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false).authFailureListeners(listener) - .authc(AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE).users(USER_1, USER_2).build(); - - @Rule - public LogsRule logsRule = new LogsRule("org.opensearch.security.auth.BackendRegistry"); - - @Test - public void shouldAuthenticateUserWhenBlockadeIsNotActive() { - try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_2))) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_OK); - } - } - - @Test - public void shouldBlockIpAddress() { - authenticateUserWithIncorrectPassword(CLIENT_IP_3, USER_2, ALLOWED_TRIES); - try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_2, CLIENT_IP_3))) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_UNAUTHORIZED); - logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_3); - } - } - - @Test - public void shouldBlockUsersWhoUseTheSameIpAddress() { - authenticateUserWithIncorrectPassword(CLIENT_IP_4, USER_1, ALLOWED_TRIES); - try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_2, CLIENT_IP_4))) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_UNAUTHORIZED); - logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_4); - } - } - - @Test - public void testUserShouldBeAbleToAuthenticateFromAnotherNotBlockedIpAddress() { - authenticateUserWithIncorrectPassword(CLIENT_IP_5, USER_1, ALLOWED_TRIES); - try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_6))) { - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_OK); - } - } - - @Test - public void shouldNotBlockIpWhenFailureAuthenticationCountIsLessThanAllowedTries() { - authenticateUserWithIncorrectPassword(CLIENT_IP_7, USER_1, ALLOWED_TRIES - 1); - try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_7))) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_OK); - } - } - - @Test - public void shouldBlockIpWhenFailureAuthenticationCountIsGreaterThanAllowedTries() { - authenticateUserWithIncorrectPassword(CLIENT_IP_8, USER_1, ALLOWED_TRIES * 2); - try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_8))) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_UNAUTHORIZED); - logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_8); - } - } - - @Test - public void shouldReleaseIpAddressLock() throws InterruptedException { - authenticateUserWithIncorrectPassword(CLIENT_IP_9, USER_1, ALLOWED_TRIES * 2); - TimeUnit.SECONDS.sleep(TIME_WINDOW_SECONDS); - try(TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_9))) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_OK); - logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_9); - } - } - - private static void authenticateUserWithIncorrectPassword(String sourceIpAddress, User user, int numberOfRequests) { - var clientConfiguration = new TestRestClientConfiguration().username(user.getName()) - .password("incorrect password").sourceInetAddress(sourceIpAddress); - try(TestRestClient client = cluster.createGenericClientRestClient(clientConfiguration)) { - for(int i = 0; i < numberOfRequests; ++i) { - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_UNAUTHORIZED); - } - } - } + private static final User USER_1 = new User("simple-user-1").roles(ALL_ACCESS); + private static final User USER_2 = new User("simple-user-2").roles(ALL_ACCESS); + + public static final int ALLOWED_TRIES = 3; + public static final int TIME_WINDOW_SECONDS = 3; + + public static final String CLIENT_IP_2 = "127.0.0.2"; + public static final String CLIENT_IP_3 = "127.0.0.3"; + public static final String CLIENT_IP_4 = "127.0.0.4"; + public static final String CLIENT_IP_5 = "127.0.0.5"; + public static final String CLIENT_IP_6 = "127.0.0.6"; + public static final String CLIENT_IP_7 = "127.0.0.7"; + public static final String CLIENT_IP_8 = "127.0.0.8"; + public static final String CLIENT_IP_9 = "127.0.0.9"; + + private static final AuthFailureListeners listener = new AuthFailureListeners().addRateLimit( + new RateLimiting("internal_authentication_backend_limiting").type("ip") + .allowedTries(ALLOWED_TRIES) + .timeWindowSeconds(TIME_WINDOW_SECONDS) + .blockExpirySeconds(2) + .maxBlockedClients(500) + .maxTrackedClients(500) + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authFailureListeners(listener) + .authc(AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE) + .users(USER_1, USER_2) + .build(); + + @Rule + public LogsRule logsRule = new LogsRule("org.opensearch.security.auth.BackendRegistry"); + + @Test + public void shouldAuthenticateUserWhenBlockadeIsNotActive() { + try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_2))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + } + + @Test + public void shouldBlockIpAddress() { + authenticateUserWithIncorrectPassword(CLIENT_IP_3, USER_2, ALLOWED_TRIES); + try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_2, CLIENT_IP_3))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_3); + } + } + + @Test + public void shouldBlockUsersWhoUseTheSameIpAddress() { + authenticateUserWithIncorrectPassword(CLIENT_IP_4, USER_1, ALLOWED_TRIES); + try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_2, CLIENT_IP_4))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_4); + } + } + + @Test + public void testUserShouldBeAbleToAuthenticateFromAnotherNotBlockedIpAddress() { + authenticateUserWithIncorrectPassword(CLIENT_IP_5, USER_1, ALLOWED_TRIES); + try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_6))) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + } + + @Test + public void shouldNotBlockIpWhenFailureAuthenticationCountIsLessThanAllowedTries() { + authenticateUserWithIncorrectPassword(CLIENT_IP_7, USER_1, ALLOWED_TRIES - 1); + try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_7))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + } + + @Test + public void shouldBlockIpWhenFailureAuthenticationCountIsGreaterThanAllowedTries() { + authenticateUserWithIncorrectPassword(CLIENT_IP_8, USER_1, ALLOWED_TRIES * 2); + try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_8))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_8); + } + } + + @Test + public void shouldReleaseIpAddressLock() throws InterruptedException { + authenticateUserWithIncorrectPassword(CLIENT_IP_9, USER_1, ALLOWED_TRIES * 2); + TimeUnit.SECONDS.sleep(TIME_WINDOW_SECONDS); + try (TestRestClient client = cluster.createGenericClientRestClient(userWithSourceIp(USER_1, CLIENT_IP_9))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + logsRule.assertThatContain("Rejecting REST request because of blocked address: /" + CLIENT_IP_9); + } + } + + private static void authenticateUserWithIncorrectPassword(String sourceIpAddress, User user, int numberOfRequests) { + var clientConfiguration = new TestRestClientConfiguration().username(user.getName()) + .password("incorrect password") + .sourceInetAddress(sourceIpAddress); + try (TestRestClient client = cluster.createGenericClientRestClient(clientConfiguration)) { + for (int i = 0; i < numberOfRequests; ++i) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/PointInTimeOperationTest.java b/src/integrationTest/java/org/opensearch/security/PointInTimeOperationTest.java index eda561185d..54b9aa4bc2 100644 --- a/src/integrationTest/java/org/opensearch/security/PointInTimeOperationTest.java +++ b/src/integrationTest/java/org/opensearch/security/PointInTimeOperationTest.java @@ -63,333 +63,364 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class PointInTimeOperationTest { - private static final String FIRST_SONG_INDEX = "song-index-1"; - private static final String FIRST_INDEX_ALIAS = "song-index-1-alias"; - private static final String SECOND_SONG_INDEX = "song-index-2"; - private static final String SECOND_INDEX_ALIAS = "song-index-2-alias"; - - private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - - /** - * User who is allowed to perform PIT operations only on the {@link #FIRST_SONG_INDEX} - */ - private static final TestSecurityConfig.User LIMITED_POINT_IN_TIME_USER = new TestSecurityConfig.User("limited_point_in_time_user") - .roles(new TestSecurityConfig.Role("limited_point_in_time_user") - .indexPermissions( - "indices:data/read/point_in_time/create", - "indices:data/read/point_in_time/delete", - "indices:data/read/search", - "indices:data/read/point_in_time/readall", //anyway user needs the all indexes permission (*) to find all pits - "indices:monitor/point_in_time/segments" //anyway user needs the all indexes permission (*) to list all pits segments - ) - .on(FIRST_SONG_INDEX)); - /** - * User who is allowed to perform PIT operations on all indices - */ - private static final TestSecurityConfig.User POINT_IN_TIME_USER = new TestSecurityConfig.User("point_in_time_user") - .roles(new TestSecurityConfig.Role("point_in_time_user") - .indexPermissions( - "indices:data/read/point_in_time/create", - "indices:data/read/point_in_time/delete", - "indices:data/read/search", - "indices:data/read/point_in_time/readall", - "indices:monitor/point_in_time/segments" - ) - .on("*")); - - private static final String ID_1 = "1"; - private static final String ID_2 = "2"; - private static final String ID_3 = "3"; - private static final String ID_4 = "4"; - - @BeforeClass - public static void createTestData() { - try(Client client = cluster.getInternalNodeClient()) { - client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(FIRST_SONG_INDEX).id(ID_1).source(SONGS[0].asMap())).actionGet(); - client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(FIRST_SONG_INDEX).id(ID_2).source(SONGS[1].asMap())).actionGet(); - client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(FIRST_SONG_INDEX).id(ID_3).source(SONGS[2].asMap())).actionGet(); - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD).indices( - FIRST_SONG_INDEX).alias(FIRST_INDEX_ALIAS))).actionGet(); - - client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(SECOND_SONG_INDEX).id(ID_4).source(SONGS[3].asMap())).actionGet(); - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new IndicesAliasesRequest.AliasActions(ADD).indices( - SECOND_SONG_INDEX).alias(SECOND_INDEX_ALIAS))).actionGet(); - } - } - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(false) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(ADMIN_USER, LIMITED_POINT_IN_TIME_USER, POINT_IN_TIME_USER) - .build(); - - @Test - public void createPit_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, FIRST_SONG_INDEX); - - CreatePitResponse createPitResponse = restHighLevelClient.createPit(createPitRequest, DEFAULT); - - assertThat(createPitResponse, isSuccessfulCreatePitResponse()); - } - } - - @Test - public void createPitWithIndexAlias_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, FIRST_INDEX_ALIAS); - - CreatePitResponse createPitResponse = restHighLevelClient.createPit(createPitRequest, DEFAULT); - - assertThat(createPitResponse, isSuccessfulCreatePitResponse()); - } - } - - @Test - public void createPit_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, SECOND_SONG_INDEX); - - assertThatThrownBy(() -> restHighLevelClient.createPit(createPitRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void createPitWithIndexAlias_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, SECOND_INDEX_ALIAS); - - assertThatThrownBy(() -> restHighLevelClient.createPit(createPitRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void listAllPits_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(POINT_IN_TIME_USER)) { - cleanUpPits(); - String firstIndexPit = createPitForIndices(FIRST_SONG_INDEX); - String secondIndexPit = createPitForIndices(SECOND_SONG_INDEX); - - GetAllPitNodesResponse getAllPitsResponse = restHighLevelClient.getAllPits(DEFAULT); - - assertThat(getAllPitsResponse, getAllResponseContainsExactlyPitWithIds(firstIndexPit, secondIndexPit)); - } - } - - @Test - public void listAllPits_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - assertThatThrownBy(() -> restHighLevelClient.getAllPits(DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void deletePit_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - String existingPitId = createPitForIndices(FIRST_SONG_INDEX); - - DeletePitResponse deletePitResponse = restHighLevelClient.deletePit(new DeletePitRequest(existingPitId), DEFAULT); - assertThat(deletePitResponse, isSuccessfulDeletePitResponse()); - assertThat(deletePitResponse, deleteResponseContainsExactlyPitWithIds(existingPitId)); - } - } - - @Test - public void deletePitCreatedWithIndexAlias_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - String existingPitId = createPitForIndices(FIRST_INDEX_ALIAS); - - DeletePitResponse deletePitResponse = restHighLevelClient.deletePit(new DeletePitRequest(existingPitId), DEFAULT); - assertThat(deletePitResponse, isSuccessfulDeletePitResponse()); - assertThat(deletePitResponse, deleteResponseContainsExactlyPitWithIds(existingPitId)); - } - } - - @Test - public void deletePit_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - String existingPitId = createPitForIndices(SECOND_SONG_INDEX); - - assertThatThrownBy(() -> restHighLevelClient.deletePit(new DeletePitRequest(existingPitId), DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void deletePitCreatedWithIndexAlias_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - String existingPitId = createPitForIndices(SECOND_INDEX_ALIAS); - - assertThatThrownBy(() -> restHighLevelClient.deletePit(new DeletePitRequest(existingPitId), DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void deleteAllPits_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(POINT_IN_TIME_USER)) { - cleanUpPits(); - String firstIndexPit = createPitForIndices(FIRST_SONG_INDEX); - String secondIndexPit = createPitForIndices(SECOND_SONG_INDEX); - - DeletePitResponse deletePitResponse = restHighLevelClient.deleteAllPits(DEFAULT); - assertThat(deletePitResponse, isSuccessfulDeletePitResponse()); - assertThat(deletePitResponse, deleteResponseContainsExactlyPitWithIds(firstIndexPit, secondIndexPit)); - } - } - - @Test - public void deleteAllPits_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - assertThatThrownBy(() -> restHighLevelClient.deleteAllPits(DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void searchWithPit_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - String existingPitId = createPitForIndices(FIRST_SONG_INDEX); - - SearchRequest searchRequest = new SearchRequest(); - searchRequest.source(new SearchSourceBuilder().pointInTimeBuilder(new PointInTimeBuilder(existingPitId))); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, searchHitsContainDocumentsInAnyOrder( - Pair.of(FIRST_SONG_INDEX, ID_1), Pair.of(FIRST_SONG_INDEX, ID_2), Pair.of(FIRST_SONG_INDEX, ID_3) - )); - } - } - - @Test - public void searchWithPitCreatedWithIndexAlias_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - String existingPitId = createPitForIndices(FIRST_INDEX_ALIAS); - - SearchRequest searchRequest = new SearchRequest(); - searchRequest.source(new SearchSourceBuilder().pointInTimeBuilder(new PointInTimeBuilder(existingPitId))); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, searchHitsContainDocumentsInAnyOrder( - Pair.of(FIRST_SONG_INDEX, ID_1), Pair.of(FIRST_SONG_INDEX, ID_2), Pair.of(FIRST_SONG_INDEX, ID_3) - )); - } - } - - @Test - public void searchWithPit_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - String existingPitId = createPitForIndices(SECOND_SONG_INDEX); - - SearchRequest searchRequest = new SearchRequest(); - searchRequest.source(new SearchSourceBuilder().pointInTimeBuilder(new PointInTimeBuilder(existingPitId))); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void searchWithPitCreatedWithIndexAlias_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { - String existingPitId = createPitForIndices(SECOND_INDEX_ALIAS); - - SearchRequest searchRequest = new SearchRequest(); - searchRequest.source(new SearchSourceBuilder().pointInTimeBuilder(new PointInTimeBuilder(existingPitId))); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void listPitSegments_positive() throws IOException { - try (TestRestClient restClient = cluster.getRestClient(LIMITED_POINT_IN_TIME_USER)) { - String existingPitId = createPitForIndices(FIRST_SONG_INDEX); - String body = String.format("{\"pit_id\":[\"%s\"]}", existingPitId); - HttpResponse response = restClient.getWithJsonBody("/_cat/pit_segments", body); - - response.assertStatusCode(OK.getStatus()); - } - } - - @Test - public void listPitSegmentsCreatedWithIndexAlias_positive() throws IOException { - try (TestRestClient restClient = cluster.getRestClient(POINT_IN_TIME_USER)) { - String existingPitId = createPitForIndices(FIRST_INDEX_ALIAS); - String body = String.format("{\"pit_id\":[\"%s\"]}", existingPitId); - HttpResponse response = restClient.getWithJsonBody("/_cat/pit_segments", body); - - response.assertStatusCode(OK.getStatus()); - } - } - - @Test - public void listPitSegments_negative() throws IOException { - try (TestRestClient restClient = cluster.getRestClient(LIMITED_POINT_IN_TIME_USER)) { - String existingPitId = createPitForIndices(SECOND_SONG_INDEX); - String body = String.format("{\"pit_id\":[\"%s\"]}", existingPitId); - HttpResponse response = restClient.getWithJsonBody("/_cat/pit_segments", body); - - response.assertStatusCode(FORBIDDEN.getStatus()); - } - } - - @Test - public void listPitSegmentsCreatedWithIndexAlias_negative() throws IOException { - try (TestRestClient restClient = cluster.getRestClient(LIMITED_POINT_IN_TIME_USER)) { - String existingPitId = createPitForIndices(SECOND_INDEX_ALIAS); - String body = String.format("{\"pit_id\":[\"%s\"]}", existingPitId); - HttpResponse response = restClient.getWithJsonBody("/_cat/pit_segments", body); - - response.assertStatusCode(FORBIDDEN.getStatus()); - } - } - - @Test - public void listAllPitSegments_positive() { - try (TestRestClient restClient = cluster.getRestClient(POINT_IN_TIME_USER)) { - HttpResponse response = restClient.get("/_cat/pit_segments/_all"); - - response.assertStatusCode(OK.getStatus()); - } - } - - @Test - public void listAllPitSegments_negative() { - try (TestRestClient restClient = cluster.getRestClient(LIMITED_POINT_IN_TIME_USER)) { - HttpResponse response = restClient.get("/_cat/pit_segments/_all"); - - response.assertStatusCode(FORBIDDEN.getStatus()); - } - } - - /** - * Creates PIT for given indices. Returns PIT id. - */ - private String createPitForIndices(String... indices) throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { - CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, indices); - - CreatePitResponse createPitResponse = restHighLevelClient.createPit(createPitRequest, DEFAULT); - - assertThat(createPitResponse, isSuccessfulCreatePitResponse()); - return createPitResponse.getId(); - } - } - - /** - * Deletes all PITs. - */ - public void cleanUpPits() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { - try { - restHighLevelClient.deleteAllPits(DEFAULT); - } catch (OpenSearchStatusException ex) { - if (ex.status() != RestStatus.NOT_FOUND) { - throw ex; - } - //tried to remove pits but no pit exists - } - } - } + private static final String FIRST_SONG_INDEX = "song-index-1"; + private static final String FIRST_INDEX_ALIAS = "song-index-1-alias"; + private static final String SECOND_SONG_INDEX = "song-index-2"; + private static final String SECOND_INDEX_ALIAS = "song-index-2-alias"; + + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + /** + * User who is allowed to perform PIT operations only on the {@link #FIRST_SONG_INDEX} + */ + private static final TestSecurityConfig.User LIMITED_POINT_IN_TIME_USER = new TestSecurityConfig.User("limited_point_in_time_user") + .roles( + new TestSecurityConfig.Role("limited_point_in_time_user").indexPermissions( + "indices:data/read/point_in_time/create", + "indices:data/read/point_in_time/delete", + "indices:data/read/search", + "indices:data/read/point_in_time/readall", // anyway user needs the all indexes permission (*) to find all pits + "indices:monitor/point_in_time/segments" // anyway user needs the all indexes permission (*) to list all pits segments + ).on(FIRST_SONG_INDEX) + ); + /** + * User who is allowed to perform PIT operations on all indices + */ + private static final TestSecurityConfig.User POINT_IN_TIME_USER = new TestSecurityConfig.User("point_in_time_user").roles( + new TestSecurityConfig.Role("point_in_time_user").indexPermissions( + "indices:data/read/point_in_time/create", + "indices:data/read/point_in_time/delete", + "indices:data/read/search", + "indices:data/read/point_in_time/readall", + "indices:monitor/point_in_time/segments" + ).on("*") + ); + + private static final String ID_1 = "1"; + private static final String ID_2 = "2"; + private static final String ID_3 = "3"; + private static final String ID_4 = "4"; + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(FIRST_SONG_INDEX).id(ID_1).source(SONGS[0].asMap())) + .actionGet(); + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(FIRST_SONG_INDEX).id(ID_2).source(SONGS[1].asMap())) + .actionGet(); + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(FIRST_SONG_INDEX).id(ID_3).source(SONGS[2].asMap())) + .actionGet(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).indices(FIRST_SONG_INDEX).alias(FIRST_INDEX_ALIAS) + ) + ) + .actionGet(); + + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(SECOND_SONG_INDEX).id(ID_4).source(SONGS[3].asMap())) + .actionGet(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new IndicesAliasesRequest.AliasActions(ADD).indices(SECOND_SONG_INDEX).alias(SECOND_INDEX_ALIAS) + ) + ) + .actionGet(); + } + } + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .anonymousAuth(false) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER, LIMITED_POINT_IN_TIME_USER, POINT_IN_TIME_USER) + .build(); + + @Test + public void createPit_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, FIRST_SONG_INDEX); + + CreatePitResponse createPitResponse = restHighLevelClient.createPit(createPitRequest, DEFAULT); + + assertThat(createPitResponse, isSuccessfulCreatePitResponse()); + } + } + + @Test + public void createPitWithIndexAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, FIRST_INDEX_ALIAS); + + CreatePitResponse createPitResponse = restHighLevelClient.createPit(createPitRequest, DEFAULT); + + assertThat(createPitResponse, isSuccessfulCreatePitResponse()); + } + } + + @Test + public void createPit_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, SECOND_SONG_INDEX); + + assertThatThrownBy(() -> restHighLevelClient.createPit(createPitRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void createPitWithIndexAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, SECOND_INDEX_ALIAS); + + assertThatThrownBy(() -> restHighLevelClient.createPit(createPitRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void listAllPits_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(POINT_IN_TIME_USER)) { + cleanUpPits(); + String firstIndexPit = createPitForIndices(FIRST_SONG_INDEX); + String secondIndexPit = createPitForIndices(SECOND_SONG_INDEX); + + GetAllPitNodesResponse getAllPitsResponse = restHighLevelClient.getAllPits(DEFAULT); + + assertThat(getAllPitsResponse, getAllResponseContainsExactlyPitWithIds(firstIndexPit, secondIndexPit)); + } + } + + @Test + public void listAllPits_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + assertThatThrownBy(() -> restHighLevelClient.getAllPits(DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void deletePit_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(FIRST_SONG_INDEX); + + DeletePitResponse deletePitResponse = restHighLevelClient.deletePit(new DeletePitRequest(existingPitId), DEFAULT); + assertThat(deletePitResponse, isSuccessfulDeletePitResponse()); + assertThat(deletePitResponse, deleteResponseContainsExactlyPitWithIds(existingPitId)); + } + } + + @Test + public void deletePitCreatedWithIndexAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(FIRST_INDEX_ALIAS); + + DeletePitResponse deletePitResponse = restHighLevelClient.deletePit(new DeletePitRequest(existingPitId), DEFAULT); + assertThat(deletePitResponse, isSuccessfulDeletePitResponse()); + assertThat(deletePitResponse, deleteResponseContainsExactlyPitWithIds(existingPitId)); + } + } + + @Test + public void deletePit_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(SECOND_SONG_INDEX); + + assertThatThrownBy( + () -> restHighLevelClient.deletePit(new DeletePitRequest(existingPitId), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + public void deletePitCreatedWithIndexAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(SECOND_INDEX_ALIAS); + + assertThatThrownBy( + () -> restHighLevelClient.deletePit(new DeletePitRequest(existingPitId), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + public void deleteAllPits_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(POINT_IN_TIME_USER)) { + cleanUpPits(); + String firstIndexPit = createPitForIndices(FIRST_SONG_INDEX); + String secondIndexPit = createPitForIndices(SECOND_SONG_INDEX); + + DeletePitResponse deletePitResponse = restHighLevelClient.deleteAllPits(DEFAULT); + assertThat(deletePitResponse, isSuccessfulDeletePitResponse()); + assertThat(deletePitResponse, deleteResponseContainsExactlyPitWithIds(firstIndexPit, secondIndexPit)); + } + } + + @Test + public void deleteAllPits_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + assertThatThrownBy(() -> restHighLevelClient.deleteAllPits(DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void searchWithPit_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(FIRST_SONG_INDEX); + + SearchRequest searchRequest = new SearchRequest(); + searchRequest.source(new SearchSourceBuilder().pointInTimeBuilder(new PointInTimeBuilder(existingPitId))); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat( + searchResponse, + searchHitsContainDocumentsInAnyOrder( + Pair.of(FIRST_SONG_INDEX, ID_1), + Pair.of(FIRST_SONG_INDEX, ID_2), + Pair.of(FIRST_SONG_INDEX, ID_3) + ) + ); + } + } + + @Test + public void searchWithPitCreatedWithIndexAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(FIRST_INDEX_ALIAS); + + SearchRequest searchRequest = new SearchRequest(); + searchRequest.source(new SearchSourceBuilder().pointInTimeBuilder(new PointInTimeBuilder(existingPitId))); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat( + searchResponse, + searchHitsContainDocumentsInAnyOrder( + Pair.of(FIRST_SONG_INDEX, ID_1), + Pair.of(FIRST_SONG_INDEX, ID_2), + Pair.of(FIRST_SONG_INDEX, ID_3) + ) + ); + } + } + + @Test + public void searchWithPit_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(SECOND_SONG_INDEX); + + SearchRequest searchRequest = new SearchRequest(); + searchRequest.source(new SearchSourceBuilder().pointInTimeBuilder(new PointInTimeBuilder(existingPitId))); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void searchWithPitCreatedWithIndexAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(SECOND_INDEX_ALIAS); + + SearchRequest searchRequest = new SearchRequest(); + searchRequest.source(new SearchSourceBuilder().pointInTimeBuilder(new PointInTimeBuilder(existingPitId))); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void listPitSegments_positive() throws IOException { + try (TestRestClient restClient = cluster.getRestClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(FIRST_SONG_INDEX); + String body = String.format("{\"pit_id\":[\"%s\"]}", existingPitId); + HttpResponse response = restClient.getWithJsonBody("/_cat/pit_segments", body); + + response.assertStatusCode(OK.getStatus()); + } + } + + @Test + public void listPitSegmentsCreatedWithIndexAlias_positive() throws IOException { + try (TestRestClient restClient = cluster.getRestClient(POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(FIRST_INDEX_ALIAS); + String body = String.format("{\"pit_id\":[\"%s\"]}", existingPitId); + HttpResponse response = restClient.getWithJsonBody("/_cat/pit_segments", body); + + response.assertStatusCode(OK.getStatus()); + } + } + + @Test + public void listPitSegments_negative() throws IOException { + try (TestRestClient restClient = cluster.getRestClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(SECOND_SONG_INDEX); + String body = String.format("{\"pit_id\":[\"%s\"]}", existingPitId); + HttpResponse response = restClient.getWithJsonBody("/_cat/pit_segments", body); + + response.assertStatusCode(FORBIDDEN.getStatus()); + } + } + + @Test + public void listPitSegmentsCreatedWithIndexAlias_negative() throws IOException { + try (TestRestClient restClient = cluster.getRestClient(LIMITED_POINT_IN_TIME_USER)) { + String existingPitId = createPitForIndices(SECOND_INDEX_ALIAS); + String body = String.format("{\"pit_id\":[\"%s\"]}", existingPitId); + HttpResponse response = restClient.getWithJsonBody("/_cat/pit_segments", body); + + response.assertStatusCode(FORBIDDEN.getStatus()); + } + } + + @Test + public void listAllPitSegments_positive() { + try (TestRestClient restClient = cluster.getRestClient(POINT_IN_TIME_USER)) { + HttpResponse response = restClient.get("/_cat/pit_segments/_all"); + + response.assertStatusCode(OK.getStatus()); + } + } + + @Test + public void listAllPitSegments_negative() { + try (TestRestClient restClient = cluster.getRestClient(LIMITED_POINT_IN_TIME_USER)) { + HttpResponse response = restClient.get("/_cat/pit_segments/_all"); + + response.assertStatusCode(FORBIDDEN.getStatus()); + } + } + + /** + * Creates PIT for given indices. Returns PIT id. + */ + private String createPitForIndices(String... indices) throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + CreatePitRequest createPitRequest = new CreatePitRequest(TimeValue.timeValueMinutes(30), false, indices); + + CreatePitResponse createPitResponse = restHighLevelClient.createPit(createPitRequest, DEFAULT); + + assertThat(createPitResponse, isSuccessfulCreatePitResponse()); + return createPitResponse.getId(); + } + } + + /** + * Deletes all PITs. + */ + public void cleanUpPits() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + try { + restHighLevelClient.deleteAllPits(DEFAULT); + } catch (OpenSearchStatusException ex) { + if (ex.status() != RestStatus.NOT_FOUND) { + throw ex; + } + // tried to remove pits but no pit exists + } + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java b/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java index 08e8394ec8..eccd9f5380 100644 --- a/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java +++ b/src/integrationTest/java/org/opensearch/security/SearchOperationTest.java @@ -197,2149 +197,2523 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class SearchOperationTest { - private static final Logger log = LogManager.getLogger(SearchOperationTest.class); - - public static final String SONG_INDEX_NAME = "song_lyrics"; - public static final String PROHIBITED_SONG_INDEX_NAME = "prohibited_song_lyrics"; - public static final String WRITE_SONG_INDEX_NAME = "write_song_index"; - - public static final String SONG_LYRICS_ALIAS = "song_lyrics_index_alias"; - public static final String PROHIBITED_SONG_ALIAS = "prohibited_song_lyrics_index_alias"; - private static final String COLLECTIVE_INDEX_ALIAS = "collective-index-alias"; - private static final String TEMPLATE_INDEX_PREFIX = "song-transcription*"; - public static final String TEMPORARY_ALIAS_NAME = "temporary-alias"; - public static final String ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001 = "alias-used-in-musical-index-template-0001"; - public static final String ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002 = "alias-used-in-musical-index-template-0002"; - public static final String ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003 = "alias-used-in-musical-index-template-0003"; - public static final String INDEX_NAME_SONG_TRANSCRIPTION_JAZZ = "song-transcription-jazz"; - - public static final String MUSICAL_INDEX_TEMPLATE = "musical-index-template"; - public static final String ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE = "alias_create_index_with_alias_positive"; - public static final String ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE = "alias_create_index_with_alias_negative"; - - public static final String UNDELETABLE_TEMPLATE_NAME = "undeletable-template-name"; - - public static final String ALIAS_FROM_UNDELETABLE_TEMPLATE = "alias-from-undeletable-template"; - - public static final String TEST_SNAPSHOT_REPOSITORY_NAME = "test-snapshot-repository"; - - public static final String UNUSED_SNAPSHOT_REPOSITORY_NAME = "unused-snapshot-repository"; - - public static final String RESTORED_SONG_INDEX_NAME = "restored_" + WRITE_SONG_INDEX_NAME; - - public static final String UPDATE_DELETE_OPERATION_INDEX_NAME = "update_delete_index"; - - public static final String DOCUMENT_TO_UPDATE_ID = "doc_to_update"; - - private static final String ID_P4 = "4"; - private static final String ID_S3 = "3"; - private static final String ID_S2 = "2"; - private static final String ID_S1 = "1"; - - static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS); - - /** - * All user read permissions are related to {@link #SONG_INDEX_NAME} index - */ - static final User LIMITED_READ_USER = new User("limited_read_user") - .roles(new Role("limited-song-reader") - .clusterPermissions("indices:data/read/mget", "indices:data/read/msearch", "indices:data/read/scroll") - .indexPermissions("indices:data/read/search", "indices:data/read/get", "indices:data/read/mget*", "indices:admin/aliases", "indices:data/read/field_caps", "indices:data/read/field_caps*") - .on(SONG_INDEX_NAME)); - - static final User LIMITED_WRITE_USER = new User("limited_write_user") - .roles(new Role("limited-write-role") - .clusterPermissions("indices:data/write/bulk", "indices:admin/template/put", "indices:admin/template/delete", "cluster:admin/repository/put", "cluster:admin/repository/delete", "cluster:admin/snapshot/create", "cluster:admin/snapshot/status", "cluster:admin/snapshot/status[nodes]", "cluster:admin/snapshot/delete", "cluster:admin/snapshot/get", "cluster:admin/snapshot/restore") - .indexPermissions("indices:data/write/index", "indices:data/write/bulk[s]", "indices:admin/create", "indices:admin/mapping/put", - "indices:data/write/update", "indices:data/write/bulk[s]", "indices:data/write/delete", "indices:data/write/bulk[s]") - .on(WRITE_SONG_INDEX_NAME), - new Role("transcription-role") - .indexPermissions("indices:data/write/index", "indices:admin/create", "indices:data/write/bulk[s]", "indices:admin/mapping/put") - .on(INDEX_NAME_SONG_TRANSCRIPTION_JAZZ), - new Role("limited-write-index-restore-role") - .indexPermissions("indices:data/write/index", "indices:admin/create", "indices:data/read/search") - .on(RESTORED_SONG_INDEX_NAME)); - - - /** - * User who is allowed read both index {@link #SONG_INDEX_NAME} and {@link #PROHIBITED_SONG_INDEX_NAME} - */ - static final User DOUBLE_READER_USER = new User("double_read_user") - .roles(new Role("full-song-reader").indexPermissions("indices:data/read/search") - .on(SONG_INDEX_NAME, PROHIBITED_SONG_INDEX_NAME)); - - static final User REINDEXING_USER = new User("reindexing_user") - .roles(new Role("song-reindexing-target-write") - .clusterPermissions("indices:data/write/reindex", "indices:data/write/bulk") - .indexPermissions("indices:admin/create", "indices:data/write/index", "indices:data/write/bulk[s]", "indices:admin/mapping/put") - .on(WRITE_SONG_INDEX_NAME), - new Role("song-reindexing-source-read") - .clusterPermissions("indices:data/read/scroll") - .indexPermissions("indices:data/read/search") - .on(SONG_INDEX_NAME)); - - private Client internalClient; - /** - * User who is allowed to update and delete documents on index {@link #UPDATE_DELETE_OPERATION_INDEX_NAME} - */ - static final User UPDATE_DELETE_USER = new User("update_delete_user") - .roles(new Role("document-updater") - .clusterPermissions("indices:data/write/bulk") - .indexPermissions("indices:data/write/update","indices:data/write/index", "indices:data/write/bulk[s]", "indices:admin/mapping/put") - .on(UPDATE_DELETE_OPERATION_INDEX_NAME), - new Role("document-remover") - .indexPermissions("indices:data/write/delete") - .on(UPDATE_DELETE_OPERATION_INDEX_NAME)) - ; - - static final String INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX = "index_operations_"; - - /** - * User who is allowed to perform index-related operations on - * indices with names prefixed by the {@link #INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX} - */ - static final User USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES = new User("index-operation-tester") - .roles(new Role("index-manager") - .indexPermissions( - "indices:admin/create", "indices:admin/get", "indices:admin/delete", "indices:admin/close", - "indices:admin/close*", "indices:admin/open", "indices:admin/resize", "indices:monitor/stats", - "indices:monitor/settings/get", "indices:admin/settings/update", "indices:admin/mapping/put", - "indices:admin/mappings/get", "indices:admin/cache/clear", "indices:admin/aliases" - ) - .on(INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("*"))); - - private static User USER_ALLOWED_TO_CREATE_INDEX = new User("user-allowed-to-create-index") - .roles(new Role("create-index-role").indexPermissions("indices:admin/create").on("*")); - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(false) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(ADMIN_USER, LIMITED_READ_USER, LIMITED_WRITE_USER, DOUBLE_READER_USER, REINDEXING_USER, UPDATE_DELETE_USER, - USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, USER_ALLOWED_TO_CREATE_INDEX) - .audit(new AuditConfiguration(true) - .compliance(new AuditCompliance().enabled(true)) - .filters(new AuditFilters().enabledRest(true).enabledTransport(true)) - ).build(); - - @Rule - public AuditLogsRule auditLogsRule = new AuditLogsRule(); - - @BeforeClass - public static void createTestData() { - try(Client client = cluster.getInternalNodeClient()) { - client.prepareIndex(SONG_INDEX_NAME).setId(ID_S1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); - client.prepareIndex(UPDATE_DELETE_OPERATION_INDEX_NAME).setId(DOCUMENT_TO_UPDATE_ID).setRefreshPolicy(IMMEDIATE).setSource("field", "value").get(); - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new AliasActions(ADD).indices(SONG_INDEX_NAME).alias(SONG_LYRICS_ALIAS))).actionGet(); - client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(SONG_INDEX_NAME).id(ID_S2).source(SONGS[1].asMap())).actionGet(); - client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(SONG_INDEX_NAME).id(ID_S3).source(SONGS[2].asMap())).actionGet(); - - client.prepareIndex(PROHIBITED_SONG_INDEX_NAME).setId(ID_P4).setSource(SONGS[3].asMap()).setRefreshPolicy(IMMEDIATE).get(); - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new AliasActions(ADD).indices(PROHIBITED_SONG_INDEX_NAME).alias(PROHIBITED_SONG_ALIAS))).actionGet(); - - client.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(new AliasActions(ADD).indices(SONG_INDEX_NAME, PROHIBITED_SONG_INDEX_NAME).alias(COLLECTIVE_INDEX_ALIAS))).actionGet(); - var createTemplateRequest = new org.opensearch.action.admin.indices.template.put.PutIndexTemplateRequest(UNDELETABLE_TEMPLATE_NAME); - createTemplateRequest.patterns(List.of("pattern-does-not-match-to-any-index")); - createTemplateRequest.alias(new Alias(ALIAS_FROM_UNDELETABLE_TEMPLATE)); - client.admin().indices().putTemplate(createTemplateRequest).actionGet(); - - client.admin().cluster().putRepository(new PutRepositoryRequest(UNUSED_SNAPSHOT_REPOSITORY_NAME).type("fs").settings(Map.of("location", cluster.getSnapshotDirPath()))).actionGet(); - } - } - - @Before - public void retrieveClusterClient() { - this.internalClient = cluster.getInternalNodeClient(); - } - - @After - public void cleanData() throws ExecutionException, InterruptedException { - Stopwatch stopwatch = Stopwatch.createStarted(); - IndicesAdminClient indices = internalClient.admin().indices(); - List indicesToBeDeleted = List.of( - WRITE_SONG_INDEX_NAME, INDEX_NAME_SONG_TRANSCRIPTION_JAZZ, RESTORED_SONG_INDEX_NAME, - INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("*") - ); - for(String indexToBeDeleted : indicesToBeDeleted) { - IndicesExistsRequest indicesExistsRequest = new IndicesExistsRequest(indexToBeDeleted); - var indicesExistsResponse = indices.exists(indicesExistsRequest).get(); - if (indicesExistsResponse.isExists()) { - DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(indexToBeDeleted); - indices.delete(deleteIndexRequest).actionGet(); - Awaitility.await().ignoreExceptions().until(() -> indices.exists(indicesExistsRequest).get().isExists() == false); - } - } - - List aliasesToBeDeleted = List.of(TEMPORARY_ALIAS_NAME, ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001, - ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002, ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE, ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE); - for(String aliasToBeDeleted : aliasesToBeDeleted) { - if(indices.exists(new IndicesExistsRequest(aliasToBeDeleted)).get().isExists()) { - AliasActions aliasAction = new AliasActions(AliasActions.Type.REMOVE).indices(SONG_INDEX_NAME).alias(aliasToBeDeleted); - internalClient.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(aliasAction)).get(); - } - } - - GetIndexTemplatesResponse response = indices.getTemplates(new GetIndexTemplatesRequest(MUSICAL_INDEX_TEMPLATE)).get(); - for(IndexTemplateMetadata metadata : response.getIndexTemplates()) { - indices.deleteTemplate(new DeleteIndexTemplateRequest(metadata.getName())).get(); - } - - ClusterAdminClient clusterClient = internalClient.admin().cluster(); - try { - clusterClient.deleteRepository(new DeleteRepositoryRequest(TEST_SNAPSHOT_REPOSITORY_NAME)).actionGet(); - } catch (RepositoryMissingException e) { - log.debug("Repository '{}' does not exist. This is expected in most of test cases", TEST_SNAPSHOT_REPOSITORY_NAME, e); - } - internalClient.close(); - log.debug("Cleaning data after test took {}", stopwatch.stop()); - } - - @Test - public void shouldSearchForDocuments_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); - } - - @Test - public void shouldSearchForDocuments_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(PROHIBITED_SONG_INDEX_NAME, QUERY_TITLE_POISON); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics/_search")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); - } - - @Test - public void shouldSearchForDocumentsViaAlias_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(SONG_LYRICS_ALIAS, QUERY_TITLE_MAGNUM_OPUS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics_index_alias/_search")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); - } - - @Test - public void shouldSearchForDocumentsViaAlias_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(PROHIBITED_SONG_ALIAS, QUERY_TITLE_POISON); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics_index_alias/_search")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); - } - - @Test - public void shouldBeAbleToSearchSongViaMultiIndexAlias_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(COLLECTIVE_INDEX_ALIAS, QUERY_TITLE_NEXT_SONG); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S3)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_NEXT_SONG)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/collective-index-alias/_search")); - auditLogsRule.assertExactlyOne(grantedPrivilege(DOUBLE_READER_USER, "SearchRequest")); - } - - @Test - public void shouldBeAbleToSearchSongViaMultiIndexAlias_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(COLLECTIVE_INDEX_ALIAS, QUERY_TITLE_POISON); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/collective-index-alias/_search")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); - } - - @Test - public void shouldBeAbleToSearchAllIndexes_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(QUERY_TITLE_MAGNUM_OPUS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(ADMIN_USER).withRestRequest(POST, "/_search")); - auditLogsRule.assertExactlyOne(grantedPrivilege(ADMIN_USER, "SearchRequest")); - } - - @Test - public void shouldBeAbleToSearchAllIndexes_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SearchRequest searchRequest = queryStringQueryRequest(QUERY_TITLE_MAGNUM_OPUS); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_search")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); - } - @Test - public void shouldBeAbleToSearchSongIndexesWithAsterisk_prohibitedSongIndex_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { - SearchRequest searchRequest = queryStringQueryRequest("*" + SONG_INDEX_NAME, QUERY_TITLE_POISON); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, PROHIBITED_SONG_INDEX_NAME, ID_P4)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_POISON)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/*song_lyrics/_search")); - auditLogsRule.assertExactlyOne(grantedPrivilege(DOUBLE_READER_USER, "SearchRequest")); - } - - @Test - public void shouldBeAbleToSearchSongIndexesWithAsterisk_singIndex_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { - SearchRequest searchRequest = queryStringQueryRequest("*" + SONG_INDEX_NAME, QUERY_TITLE_NEXT_SONG); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S3)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_NEXT_SONG)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/*song_lyrics/_search")); - auditLogsRule.assertExactlyOne(grantedPrivilege(DOUBLE_READER_USER, "SearchRequest")); - } - - @Test - public void shouldBeAbleToSearchSongIndexesWithAsterisk_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SearchRequest searchRequest = queryStringQueryRequest("*" + SONG_INDEX_NAME, QUERY_TITLE_NEXT_SONG); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/*song_lyrics/_search")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); - } - - @Test - public void shouldFindSongUsingDslQuery_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SearchRequest searchRequest = new SearchRequest(SONG_INDEX_NAME); - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); - boolQueryBuilder.filter(QueryBuilders.regexpQuery(FIELD_ARTIST, "f.+")); - boolQueryBuilder.filter(new MatchQueryBuilder(FIELD_TITLE, TITLE_MAGNUM_OPUS)); - searchSourceBuilder.query(boolQueryBuilder); - searchRequest.source(searchSourceBuilder); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); - } - - @Test - public void shouldFindSongUsingDslQuery_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SearchRequest searchRequest = new SearchRequest(PROHIBITED_SONG_INDEX_NAME); - SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); - BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); - boolQueryBuilder.filter(QueryBuilders.regexpQuery(FIELD_ARTIST, "n.+")); - boolQueryBuilder.filter(new MatchQueryBuilder(FIELD_TITLE, TITLE_POISON)); - searchSourceBuilder.query(boolQueryBuilder); - searchRequest.source(searchSourceBuilder); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics/_search")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); - } - - @Test - public void shouldPerformSearchWithAllIndexAlias_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { - SearchRequest searchRequest = queryStringQueryRequest("_all", QUERY_TITLE_MAGNUM_OPUS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); - assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(ADMIN_USER).withRestRequest(POST, "/_all/_search")); - auditLogsRule.assertExactlyOne(grantedPrivilege(ADMIN_USER, "SearchRequest")); - } - - @Test - public void shouldPerformSearchWithAllIndexAlias_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SearchRequest searchRequest = queryStringQueryRequest("_all", QUERY_TITLE_MAGNUM_OPUS); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_all/_search")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); - } - - @Test - public void shouldScrollOverSearchResults_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SearchRequest searchRequest = searchRequestWithScroll(SONG_INDEX_NAME, 2); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, containNotEmptyScrollingId()); - - SearchScrollRequest scrollRequest = getSearchScrollRequest(searchResponse); - - SearchResponse scrollResponse = restHighLevelClient.scroll(scrollRequest, DEFAULT); - assertThat(scrollResponse, isSuccessfulSearchResponse()); - assertThat(scrollResponse, containNotEmptyScrollingId()); - assertThat(scrollResponse, numberOfTotalHitsIsEqualTo(3)); - assertThat(scrollResponse, numberOfHitsInPageIsEqualTo(1)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_search/scroll")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchScrollRequest")); - } - - @Test - public void shouldScrollOverSearchResults_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { - SearchRequest searchRequest = searchRequestWithScroll(SONG_INDEX_NAME, 2); - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, containNotEmptyScrollingId()); - - SearchScrollRequest scrollRequest = getSearchScrollRequest(searchResponse); - - assertThatThrownBy(() -> restHighLevelClient.scroll(scrollRequest, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/song_lyrics/_search")); - auditLogsRule.assertExactlyOne(grantedPrivilege(DOUBLE_READER_USER, "SearchRequest")); - auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/_search/scroll")); - auditLogsRule.assertExactlyOne(missingPrivilege(DOUBLE_READER_USER, "SearchScrollRequest")); - } - - @Test - public void shouldGetDocument_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - GetResponse response = restHighLevelClient.get(new GetRequest(SONG_INDEX_NAME, ID_S1), DEFAULT); - - assertThat(response, containDocument(SONG_INDEX_NAME, ID_S1)); - assertThat(response, documentContainField(FIELD_TITLE, TITLE_MAGNUM_OPUS)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/song_lyrics/_doc/1")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "GetRequest")); - } - - @Test - public void shouldGetDocument_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - GetRequest getRequest = new GetRequest(PROHIBITED_SONG_INDEX_NAME, ID_P4); - assertThatThrownBy(() -> restHighLevelClient.get(getRequest, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/prohibited_song_lyrics/_doc/4")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "GetRequest")); - } - - @Test - public void shouldPerformMultiGetDocuments_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - MultiGetRequest request = new MultiGetRequest(); - request.add(new Item(SONG_INDEX_NAME, ID_S1)); - request.add(new Item(SONG_INDEX_NAME, ID_S2)); - - MultiGetResponse response = restHighLevelClient.mget(request, DEFAULT); - - assertThat(response, is(notNullValue())); - assertThat(response, isSuccessfulMultiGetResponse()); - assertThat(response, numberOfGetItemResponsesIsEqualTo(2)); - - MultiGetItemResponse[] responses = response.getResponses(); - assertThat(responses[0].getResponse(), allOf( - containDocument(SONG_INDEX_NAME, ID_S1), - documentContainField(FIELD_TITLE, TITLE_MAGNUM_OPUS)) - ); - assertThat(responses[1].getResponse(), allOf( - containDocument(SONG_INDEX_NAME, ID_S2), - documentContainField(FIELD_TITLE, TITLE_SONG_1_PLUS_1)) - ); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_mget")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiGetRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiGetShardRequest")); - } - - @Test - public void shouldPerformMultiGetDocuments_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { - MultiGetRequest request = new MultiGetRequest(); - request.add(new Item(SONG_INDEX_NAME, ID_S1)); - - assertThatThrownBy(() -> restHighLevelClient.mget(request, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/_mget")); - auditLogsRule.assertExactlyOne(missingPrivilege(DOUBLE_READER_USER, "MultiGetRequest")); - } - - @Test - public void shouldPerformMultiGetDocuments_partiallyPositive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - MultiGetRequest request = new MultiGetRequest(); - request.add(new Item(SONG_INDEX_NAME, ID_S1)); - request.add(new Item(PROHIBITED_SONG_INDEX_NAME, ID_P4)); - - MultiGetResponse response = restHighLevelClient.mget(request, DEFAULT); - - assertThat(request, notNullValue()); - assertThat(response, not(isSuccessfulMultiGetResponse())); - assertThat(response, numberOfGetItemResponsesIsEqualTo(2)); - - MultiGetItemResponse[] responses = response.getResponses(); - assertThat(responses, arrayContaining( - hasProperty("failure", nullValue()), - hasProperty("failure", notNullValue()) - )); - assertThat(responses[1].getFailure().getFailure(), statusException(INTERNAL_SERVER_ERROR)); - assertThat(responses[1].getFailure().getFailure(), errorMessageContain("security_exception")); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_mget")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiGetRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiGetShardRequest").withIndex(SONG_INDEX_NAME)); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "MultiGetShardRequest").withIndex(PROHIBITED_SONG_INDEX_NAME)); - } - - @Test - public void shouldBeAllowedToPerformMulitSearch_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - MultiSearchRequest request = new MultiSearchRequest(); - request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS)); - request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_NEXT_SONG)); - - MultiSearchResponse response = restHighLevelClient.msearch(request, DEFAULT); - - assertThat(response, notNullValue()); - assertThat(response, isSuccessfulMultiSearchResponse()); - assertThat(response, numberOfSearchItemResponsesIsEqualTo(2)); - - MultiSearchResponse.Item[] responses = response.getResponses(); - - assertThat(responses[0].getResponse(), searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); - assertThat(responses[0].getResponse(), searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); - assertThat(responses[1].getResponse(), searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_NEXT_SONG)); - assertThat(responses[1].getResponse(), searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S3)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_msearch")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiSearchRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); - } - - @Test - public void shouldBeAllowedToPerformMulitSearch_partiallyPositive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - MultiSearchRequest request = new MultiSearchRequest(); - request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS)); - request.add(queryStringQueryRequest(PROHIBITED_SONG_INDEX_NAME, QUERY_TITLE_POISON)); - - MultiSearchResponse response = restHighLevelClient.msearch(request, DEFAULT); - - assertThat(response, notNullValue()); - assertThat(response, not(isSuccessfulMultiSearchResponse())); - assertThat(response, numberOfSearchItemResponsesIsEqualTo(2)); - - MultiSearchResponse.Item[] responses = response.getResponses(); - assertThat(responses[0].getFailure(), nullValue()); - assertThat(responses[1].getFailure(), statusException(INTERNAL_SERVER_ERROR)); - assertThat(responses[1].getFailure(), errorMessageContain("security_exception")); - assertThat(responses[1].getResponse(), nullValue()); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_msearch")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiSearchRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest").withIndex(SONG_INDEX_NAME)); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest").withIndex(PROHIBITED_SONG_INDEX_NAME)); - } - - @Test - public void shouldBeAllowedToPerformMulitSearch_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { - MultiSearchRequest request = new MultiSearchRequest(); - request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS)); - request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_NEXT_SONG)); - - assertThatThrownBy(() -> restHighLevelClient.msearch(request, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/_msearch")); - auditLogsRule.assertExactlyOne(missingPrivilege(DOUBLE_READER_USER, "MultiSearchRequest")); - } - - @Test - public void shouldAggregateDataAndComputeAverage_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - final String aggregationName = "averageStars"; - SearchRequest searchRequest = averageAggregationRequest(SONG_INDEX_NAME, aggregationName, FIELD_STARS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest").withIndex(SONG_INDEX_NAME)); - } - - @Test - public void shouldAggregateDataAndComputeAverage_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SearchRequest searchRequest = averageAggregationRequest(PROHIBITED_SONG_INDEX_NAME, "averageStars", FIELD_STARS); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics/_search")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest").withIndex(PROHIBITED_SONG_INDEX_NAME)); - } - - @Test - public void shouldPerformStatAggregation_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - final String aggregationName = "statsStars"; - SearchRequest searchRequest = statsAggregationRequest(SONG_INDEX_NAME, aggregationName, FIELD_STARS); - - SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "stats")); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); - } - - @Test - public void shouldPerformStatAggregation_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SearchRequest searchRequest = statsAggregationRequest(PROHIBITED_SONG_INDEX_NAME, "statsStars", FIELD_STARS); - - assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics/_search")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); - } - - @Test - public void shouldIndexDocumentInBulkRequest_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - BulkRequest bulkRequest = new BulkRequest(); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); - bulkRequest.setRefreshPolicy(IMMEDIATE); - - BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); - - assertThat(response, successBulkResponse()); - assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, "one")); - assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, "two")); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "one", FIELD_TITLE, TITLE_MAGNUM_OPUS)); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, TITLE_SONG_1_PLUS_1)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertAtLeast(4, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER));//sometimes 4 or 6 - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest"));//sometimes 2 or 4 - } - - @Test - public void shouldIndexDocumentInBulkRequest_partiallyPositive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - BulkRequest bulkRequest = new BulkRequest(); - bulkRequest.add(new IndexRequest(SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); - bulkRequest.setRefreshPolicy(IMMEDIATE); - - BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); - - assertThat(response, bulkResponseContainExceptions(0, allOf( - statusException(INTERNAL_SERVER_ERROR), - errorMessageContain("security_exception") - ))); - assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, "two")); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, TITLE_SONG_1_PLUS_1)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest").withIndex(SONG_INDEX_NAME)); - } - - @Test - public void shouldIndexDocumentInBulkRequest_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - BulkRequest bulkRequest = new BulkRequest(); - bulkRequest.add(new IndexRequest(SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); - bulkRequest.add(new IndexRequest(SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); - bulkRequest.setRefreshPolicy(IMMEDIATE); - - BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); - - assertThat(response, allOf( - failureBulkResponse(), - bulkResponseContainExceptions(statusException(INTERNAL_SERVER_ERROR)), - bulkResponseContainExceptions(errorMessageContain("security_exception")) - )); - assertThat(internalClient, not(clusterContainsDocument(SONG_INDEX_NAME, "one"))); - assertThat(internalClient, not(clusterContainsDocument(SONG_INDEX_NAME, "two"))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest").withIndex(SONG_INDEX_NAME)); - } - - @Test - public void shouldUpdateDocumentsInBulk_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - final String titleOne = "shape of my mind"; - final String titleTwo = "forgiven"; - BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); - restHighLevelClient.bulk(bulkRequest, DEFAULT); - bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); - bulkRequest.add(new UpdateRequest(WRITE_SONG_INDEX_NAME, "one").doc(Map.of(FIELD_TITLE, titleOne))); - bulkRequest.add(new UpdateRequest(WRITE_SONG_INDEX_NAME, "two").doc(Map.of(FIELD_TITLE, titleTwo))); - - BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); - - assertThat(response, successBulkResponse()); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "one", FIELD_TITLE, titleOne)); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, titleTwo)); - } - auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - - } - - @Test - public void shouldUpdateDocumentsInBulk_partiallyPositive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - final String titleOne = "shape of my mind"; - BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); - restHighLevelClient.bulk(bulkRequest, DEFAULT); - bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); - bulkRequest.add(new UpdateRequest(WRITE_SONG_INDEX_NAME, "one").doc(Map.of(FIELD_TITLE, titleOne))); - bulkRequest.add(new UpdateRequest(SONG_INDEX_NAME, ID_S2).doc(Map.of(FIELD_TITLE, "forgiven"))); - - BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); - - assertThat(response, bulkResponseContainExceptions(1, allOf( - statusException(INTERNAL_SERVER_ERROR), - errorMessageContain("security_exception") - ))); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "one", FIELD_TITLE, titleOne)); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(SONG_INDEX_NAME, ID_S2, FIELD_TITLE, TITLE_SONG_1_PLUS_1)); - } - auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest").withIndex(SONG_INDEX_NAME)); - } - - @Test - public void shouldUpdateDocumentsInBulk_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); - bulkRequest.add(new UpdateRequest(SONG_INDEX_NAME, ID_S1).doc(Map.of(FIELD_TITLE, "shape of my mind"))); - bulkRequest.add(new UpdateRequest(SONG_INDEX_NAME, ID_S2).doc(Map.of(FIELD_TITLE, "forgiven"))); - - BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); - - assertThat(response, allOf( - failureBulkResponse(), - bulkResponseContainExceptions(statusException(INTERNAL_SERVER_ERROR)), - bulkResponseContainExceptions(errorMessageContain("security_exception")) - )); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(SONG_INDEX_NAME, ID_S1, FIELD_TITLE, TITLE_MAGNUM_OPUS)); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(SONG_INDEX_NAME, ID_S2, FIELD_TITLE, TITLE_SONG_1_PLUS_1)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest")); - } - - @Test - public void shouldDeleteDocumentInBulk_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("three").source(SONGS[2].asMap())); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("four").source(SONGS[3].asMap())); - assertThat(restHighLevelClient.bulk(bulkRequest, DEFAULT), successBulkResponse()); - bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); - bulkRequest.add(new DeleteRequest(WRITE_SONG_INDEX_NAME, "one")); - bulkRequest.add(new DeleteRequest(WRITE_SONG_INDEX_NAME, "three")); - - BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); - - assertThat(response, successBulkResponse()); - assertThat(internalClient, not(clusterContainsDocument(WRITE_SONG_INDEX_NAME, "one"))); - assertThat(internalClient, not(clusterContainsDocument(WRITE_SONG_INDEX_NAME, "three"))); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, TITLE_SONG_1_PLUS_1)); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "four", FIELD_TITLE, TITLE_POISON)); - } - auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - } - - @Test - public void shouldDeleteDocumentInBulk_partiallyPositive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); - assertThat(restHighLevelClient.bulk(bulkRequest, DEFAULT), successBulkResponse()); - bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); - bulkRequest.add(new DeleteRequest(WRITE_SONG_INDEX_NAME, "one")); - bulkRequest.add(new DeleteRequest(SONG_INDEX_NAME, ID_S3)); - - BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); - assertThat(internalClient, not(clusterContainsDocument(WRITE_SONG_INDEX_NAME, "one"))); - - assertThat(response, bulkResponseContainExceptions(1, allOf( - statusException(INTERNAL_SERVER_ERROR), - errorMessageContain("security_exception") - ))); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, TITLE_SONG_1_PLUS_1)); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(SONG_INDEX_NAME, ID_S3, FIELD_TITLE, TITLE_NEXT_SONG)); - } - auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest")); - } - - @Test - public void shouldDeleteDocumentInBulk_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); - bulkRequest.add(new DeleteRequest(SONG_INDEX_NAME, ID_S1)); - bulkRequest.add(new DeleteRequest(SONG_INDEX_NAME, ID_S3)); - - BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); - - assertThat(response, allOf( - failureBulkResponse(), - bulkResponseContainExceptions(statusException(INTERNAL_SERVER_ERROR)), - bulkResponseContainExceptions(errorMessageContain("security_exception")) - )); - assertThat(internalClient, clusterContainsDocument(SONG_INDEX_NAME, ID_S1)); - assertThat(internalClient, clusterContainsDocument(SONG_INDEX_NAME, ID_S3)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest")); - - } - - @Test - public void shouldReindexDocuments_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { - ReindexRequest reindexRequest = new ReindexRequest().setSourceIndices(SONG_INDEX_NAME).setDestIndex(WRITE_SONG_INDEX_NAME); - - BulkByScrollResponse response = restHighLevelClient.reindex(reindexRequest, DEFAULT); - - assertThat(response, notNullValue()); - assertThat(response.getBulkFailures(), empty()); - assertThat(response.getSearchFailures(), empty()); - assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, ID_S1)); - assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, ID_S2)); - assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, ID_S3)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(REINDEXING_USER).withRestRequest(POST, "/_reindex")); - auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "ReindexRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "SearchRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(REINDEXING_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(REINDEXING_USER, "PutMappingRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "SearchScrollRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(REINDEXING_USER)); - auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "ClearScrollRequest")); - } - - @Test - public void shouldReindexDocuments_negativeSource() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { - ReindexRequest reindexRequest = new ReindexRequest().setSourceIndices(PROHIBITED_SONG_INDEX_NAME).setDestIndex(WRITE_SONG_INDEX_NAME); - - assertThatThrownBy(() -> restHighLevelClient.reindex(reindexRequest, DEFAULT), statusException(FORBIDDEN)); - assertThat(internalClient, not(clusterContainsDocument(WRITE_SONG_INDEX_NAME, ID_P4))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(REINDEXING_USER).withRestRequest(POST, "/_reindex")); - auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "ReindexRequest")); - auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "SearchRequest")); - } - - @Test - public void shouldReindexDocuments_negativeDestination() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { - ReindexRequest reindexRequest = new ReindexRequest().setSourceIndices(SONG_INDEX_NAME).setDestIndex(PROHIBITED_SONG_INDEX_NAME); - - assertThatThrownBy(() -> restHighLevelClient.reindex(reindexRequest, DEFAULT), statusException(FORBIDDEN)); - assertThat(internalClient, not(clusterContainsDocument(PROHIBITED_SONG_INDEX_NAME, ID_S1))); - assertThat(internalClient, not(clusterContainsDocument(PROHIBITED_SONG_INDEX_NAME, ID_S2))); - assertThat(internalClient, not(clusterContainsDocument(PROHIBITED_SONG_INDEX_NAME, ID_S3))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(REINDEXING_USER).withRestRequest(POST, "/_reindex")); - auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "ReindexRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "SearchRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "BulkRequest")); - auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "BulkShardRequest")); - auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "ClearScrollRequest")); - } - - @Test - public void shouldReindexDocuments_negativeSourceAndDestination() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { - ReindexRequest reindexRequest = new ReindexRequest().setSourceIndices(PROHIBITED_SONG_INDEX_NAME).setDestIndex(SONG_INDEX_NAME); - - assertThatThrownBy(() -> restHighLevelClient.reindex(reindexRequest, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(REINDEXING_USER).withRestRequest(POST, "/_reindex")); - auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "ReindexRequest")); - auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "SearchRequest")); - } - - @Test - public void shouldUpdateDocument_positive() throws IOException { - String newField = "newField"; - String newValue = "newValue"; - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(UPDATE_DELETE_USER)) { - UpdateRequest updateRequest = new UpdateRequest(UPDATE_DELETE_OPERATION_INDEX_NAME, DOCUMENT_TO_UPDATE_ID) - .doc(newField, newValue).setRefreshPolicy(IMMEDIATE); - - UpdateResponse response = restHighLevelClient.update(updateRequest, DEFAULT); - - assertThat(response, isSuccessfulUpdateResponse()); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(UPDATE_DELETE_OPERATION_INDEX_NAME, DOCUMENT_TO_UPDATE_ID, newField, newValue)); - } - } - @Test - public void shouldUpdateDocument_negative() throws IOException { - String newField = "newField"; - String newValue = "newValue"; - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(UPDATE_DELETE_USER)) { - UpdateRequest updateRequest = new UpdateRequest(PROHIBITED_SONG_INDEX_NAME, DOCUMENT_TO_UPDATE_ID).doc(newField, newValue).setRefreshPolicy(IMMEDIATE); - - assertThatThrownBy(() -> restHighLevelClient.update(updateRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldDeleteDocument_positive() throws IOException { - String docId = "shouldDeleteDocument_positive"; - try(Client client = cluster.getInternalNodeClient()){ - client.index(new IndexRequest(UPDATE_DELETE_OPERATION_INDEX_NAME).id(docId).source("field", "value").setRefreshPolicy(IMMEDIATE)).actionGet(); - assertThat(internalClient, clusterContainsDocument(UPDATE_DELETE_OPERATION_INDEX_NAME, docId)); - } - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(UPDATE_DELETE_USER)) { - DeleteRequest deleteRequest = new DeleteRequest(UPDATE_DELETE_OPERATION_INDEX_NAME, docId).setRefreshPolicy(IMMEDIATE); - - DeleteResponse response = restHighLevelClient.delete(deleteRequest, DEFAULT); - - assertThat(response, isSuccessfulDeleteResponse()); - assertThat(internalClient, not(clusterContainsDocument(UPDATE_DELETE_OPERATION_INDEX_NAME, docId))); - } - } - @Test - public void shouldDeleteDocument_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(UPDATE_DELETE_USER)) { - DeleteRequest deleteRequest = new DeleteRequest(PROHIBITED_SONG_INDEX_NAME, ID_S1).setRefreshPolicy(IMMEDIATE); - - assertThatThrownBy(() -> restHighLevelClient.delete(deleteRequest, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldCreateAlias_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - AliasActions aliasAction = new AliasActions(ADD).indices(SONG_INDEX_NAME).alias(TEMPORARY_ALIAS_NAME); - IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); - - var response = restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT); - - assertThat(response, notNullValue()); - assertThat(response.isAcknowledged(), equalTo(true)); - assertThat(internalClient, clusterContainsDocument(TEMPORARY_ALIAS_NAME, ID_S1)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); - auditLogsRule.assertExactly(2, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_READ_USER)); - } - - @Test - public void shouldCreateAlias_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - AliasActions aliasAction = new AliasActions(ADD).indices(PROHIBITED_SONG_INDEX_NAME).alias(TEMPORARY_ALIAS_NAME); - IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); - - assertThatThrownBy(() -> restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT), statusException(FORBIDDEN)); - - assertThat(internalClient, not(clusterContainsDocument(TEMPORARY_ALIAS_NAME, ID_P4))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); - } - - @Test - public void shouldDeleteAlias_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - AliasActions aliasAction = new AliasActions(ADD).indices(SONG_INDEX_NAME).alias(TEMPORARY_ALIAS_NAME); - IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); - restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT); - aliasAction = new AliasActions(REMOVE).indices(SONG_INDEX_NAME).alias(TEMPORARY_ALIAS_NAME); - indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); - - var response = restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT); - - assertThat(response, notNullValue()); - assertThat(response.isAcknowledged(), equalTo(true)); - assertThat(internalClient, not(clusterContainsDocument(TEMPORARY_ALIAS_NAME, ID_S1))); - } - auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); - auditLogsRule.assertExactly(4, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_READ_USER)); - } - - @Test - public void shouldDeleteAlias_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - AliasActions aliasAction = new AliasActions(REMOVE).indices(PROHIBITED_SONG_INDEX_NAME).alias(PROHIBITED_SONG_ALIAS); - IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); - - assertThatThrownBy(() -> restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT), statusException(FORBIDDEN)); - - assertThat(internalClient, clusterContainsDocument(PROHIBITED_SONG_INDEX_NAME, ID_P4)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); - } - - @Test - public void shouldCreateIndexTemplate_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - PutIndexTemplateRequest request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE) - .patterns(List.of(TEMPLATE_INDEX_PREFIX)) - .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001)) - .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002)); - - var response = restHighLevelClient.indices().putTemplate(request, DEFAULT); - - assertThat(response, notNullValue()); - assertThat(response.isAcknowledged(), equalTo(true)); - assertThat(internalClient, clusterContainTemplate(MUSICAL_INDEX_TEMPLATE )); - String documentId = "0001"; - IndexRequest indexRequest = new IndexRequest(INDEX_NAME_SONG_TRANSCRIPTION_JAZZ).id(documentId).source(SONGS[0].asMap()) - .setRefreshPolicy(IMMEDIATE); - restHighLevelClient.index(indexRequest, DEFAULT); - assertThat(internalClient, clusterContainsDocument(INDEX_NAME_SONG_TRANSCRIPTION_JAZZ, documentId)); - assertThat(internalClient, clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001, documentId)); - assertThat(internalClient, clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002, documentId)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_template/musical-index-template")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/song-transcription-jazz/_doc/0001")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "IndexRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(8, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - } - - @Test - public void shouldCreateIndexTemplate_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - PutIndexTemplateRequest request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE) - .patterns(List.of(TEMPLATE_INDEX_PREFIX)) - .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001)) - .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002)); - - assertThatThrownBy(() -> restHighLevelClient.indices().putTemplate(request, DEFAULT), statusException(FORBIDDEN)); - assertThat(internalClient, not(clusterContainTemplate(MUSICAL_INDEX_TEMPLATE ))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(PUT, "/_template/musical-index-template")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "PutIndexTemplateRequest")); - } - - @Test - public void shouldDeleteTemplate_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - PutIndexTemplateRequest request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE) - .patterns(List.of(TEMPLATE_INDEX_PREFIX)); - restHighLevelClient.indices().putTemplate(request, DEFAULT); - assertThat(internalClient, clusterContainTemplate(MUSICAL_INDEX_TEMPLATE)); - DeleteIndexTemplateRequest deleteRequest = new DeleteIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE); - - var response = restHighLevelClient.indices().deleteTemplate(deleteRequest, DEFAULT); - - assertThat(response, notNullValue()); - assertThat(response.isAcknowledged(), equalTo(true)); - assertThat(internalClient, not(clusterContainTemplate(MUSICAL_INDEX_TEMPLATE))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_template/musical-index-template")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(DELETE, "/_template/musical-index-template")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "DeleteIndexTemplateRequest")); - auditLogsRule.assertExactly(4, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - } - - @Test - public void shouldDeleteTemplate_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - DeleteIndexTemplateRequest deleteRequest = new DeleteIndexTemplateRequest(UNDELETABLE_TEMPLATE_NAME); - - assertThatThrownBy(() -> restHighLevelClient.indices().deleteTemplate(deleteRequest, DEFAULT), statusException(FORBIDDEN)); - - assertThat(internalClient, clusterContainTemplate(UNDELETABLE_TEMPLATE_NAME)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(DELETE, "/_template/undeletable-template-name")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "DeleteIndexTemplateRequest")); - } - - @Test - public void shouldUpdateTemplate_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - PutIndexTemplateRequest request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE) - .patterns(List.of(TEMPLATE_INDEX_PREFIX)) - .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001)) - .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002)); - restHighLevelClient.indices().putTemplate(request, DEFAULT); - assertThat(internalClient, clusterContainTemplate(MUSICAL_INDEX_TEMPLATE)); - request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE) - .patterns(List.of(TEMPLATE_INDEX_PREFIX)) - .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003)); - - var response = restHighLevelClient.indices().putTemplate(request, DEFAULT); - - assertThat(response, notNullValue()); - assertThat(response.isAcknowledged(), equalTo(true)); - String documentId = "000one"; - IndexRequest indexRequest = new IndexRequest(INDEX_NAME_SONG_TRANSCRIPTION_JAZZ).id(documentId).source(SONGS[0].asMap()) - .setRefreshPolicy(IMMEDIATE); - restHighLevelClient.index(indexRequest, DEFAULT); - assertThat(internalClient, clusterContainTemplate(MUSICAL_INDEX_TEMPLATE)); - assertThat(internalClient, clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003, documentId)); - assertThat(internalClient, not(clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001, documentId))); - assertThat(internalClient, not(clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002, documentId))); - } - auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_template/musical-index-template")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/song-transcription-jazz/_doc/000one")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "IndexRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(10, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - } - @Test - public void shouldUpdateTemplate_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - PutIndexTemplateRequest request = new PutIndexTemplateRequest(UNDELETABLE_TEMPLATE_NAME) - .patterns(List.of(TEMPLATE_INDEX_PREFIX)) - .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003)); - - assertThatThrownBy(() -> restHighLevelClient.indices().putTemplate(request, DEFAULT), statusException(FORBIDDEN)); - assertThat(internalClient, clusterContainTemplateWithAlias(UNDELETABLE_TEMPLATE_NAME, ALIAS_FROM_UNDELETABLE_TEMPLATE)); - assertThat(internalClient, not(clusterContainTemplateWithAlias(UNDELETABLE_TEMPLATE_NAME, ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(PUT, "/_template/undeletable-template-name")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "PutIndexTemplateRequest")); - } - - @Test - public void shouldGetFieldCapabilitiesForAllIndexes_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { - FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().fields(FIELD_TITLE); - - FieldCapabilitiesResponse response = restHighLevelClient.fieldCaps(request, DEFAULT); - - assertThat(response, notNullValue()); - assertThat(response, containsExactlyIndices(SONG_INDEX_NAME, PROHIBITED_SONG_INDEX_NAME, UPDATE_DELETE_OPERATION_INDEX_NAME)); - assertThat(response, numberOfFieldsIsEqualTo(1)); - assertThat(response, containsFieldWithNameAndType(FIELD_TITLE, "text")); - } - auditLogsRule.assertExactlyOne(userAuthenticated(ADMIN_USER).withRestRequest(GET, "/_field_caps")); - auditLogsRule.assertExactlyOne(grantedPrivilege(ADMIN_USER, "FieldCapabilitiesRequest")); - auditLogsRule.assertExactly(3, grantedPrivilege(ADMIN_USER, "FieldCapabilitiesIndexRequest")); - } - - @Test - public void shouldGetFieldCapabilitiesForAllIndexes_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().fields(FIELD_TITLE); - - assertThatThrownBy(() -> restHighLevelClient.fieldCaps(request, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/_field_caps")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "FieldCapabilitiesRequest")); - } - - @Test - public void shouldGetFieldCapabilitiesForParticularIndex_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(SONG_INDEX_NAME).fields(FIELD_TITLE); - - FieldCapabilitiesResponse response = restHighLevelClient.fieldCaps(request, DEFAULT); - - assertThat(response, notNullValue()); - assertThat(response, containsExactlyIndices(SONG_INDEX_NAME)); - assertThat(response, numberOfFieldsIsEqualTo(1)); - assertThat(response, containsFieldWithNameAndType(FIELD_TITLE, "text")); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/song_lyrics/_field_caps")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "FieldCapabilitiesRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "FieldCapabilitiesIndexRequest")); - } - - @Test - public void shouldGetFieldCapabilitiesForParticularIndex_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(PROHIBITED_SONG_INDEX_NAME).fields(FIELD_TITLE); - - assertThatThrownBy(() -> restHighLevelClient.fieldCaps(request, DEFAULT), statusException(FORBIDDEN)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/prohibited_song_lyrics/_field_caps")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "FieldCapabilitiesRequest")); - } - - @Test - public void shouldCreateSnapshotRepository_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); - String snapshotDirPath = cluster.getSnapshotDirPath(); - - var response = steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotDirPath, "fs"); - - assertThat(response, notNullValue()); - assertThat(response.isAcknowledged(), equalTo(true)); - assertThat(internalClient, clusterContainsSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - } - - @Test - public void shouldCreateSnapshotRepository_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); - String snapshotDirPath = cluster.getSnapshotDirPath(); - - assertThatThrownBy(() -> steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotDirPath, "fs"), statusException(FORBIDDEN)); - assertThat(internalClient, not(clusterContainsSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "PutRepositoryRequest")); - } - - @Test - public void shouldDeleteSnapshotRepository_positive() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); - steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); - assertThat(internalClient, clusterContainsSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME)); - - var response = steps.deleteSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME); - - assertThat(response, notNullValue()); - assertThat(response.isAcknowledged(), equalTo(true)); - assertThat(internalClient, not(clusterContainsSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(DELETE, "/_snapshot/test-snapshot-repository")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "DeleteRepositoryRequest")); - } - - @Test - public void shouldDeleteSnapshotRepository_negative() throws IOException { - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); - - assertThatThrownBy(() -> steps.deleteSnapshotRepository(UNUSED_SNAPSHOT_REPOSITORY_NAME), statusException(FORBIDDEN)); - assertThat(internalClient, clusterContainsSnapshotRepository(UNUSED_SNAPSHOT_REPOSITORY_NAME)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(DELETE, "/_snapshot/unused-snapshot-repository")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "DeleteRepositoryRequest")); - } - - @Test //Bug which can be reproduced with the below test: https://github.com/opensearch-project/security/issues/2169 - public void shouldCreateSnapshot_positive() throws IOException { - final String snapshotName = "snapshot-positive-test"; - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); - steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); - - CreateSnapshotResponse response = steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME); - - assertThat(response, notNullValue()); - assertThat(response.status(), equalTo(RestStatus.ACCEPTED)); - steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); - assertThat(internalClient, clusterContainSuccessSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/snapshot-positive-test")); - auditLogsRule.assertAtLeast(1, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/snapshot-positive-test")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); - } - - @Test - public void shouldCreateSnapshot_negative() throws IOException { - final String snapshotName = "snapshot-negative-test"; - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); - - assertThatThrownBy(() -> steps.createSnapshot(UNUSED_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME), statusException(FORBIDDEN)); - - assertThat(internalClient, snapshotInClusterDoesNotExists(UNUSED_SNAPSHOT_REPOSITORY_NAME, snapshotName)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(PUT, "/_snapshot/unused-snapshot-repository/snapshot-negative-test")); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "CreateSnapshotRequest")); - } - - @Test - public void shouldDeleteSnapshot_positive() throws IOException { - String snapshotName = "delete-snapshot-positive"; - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); - restHighLevelClient.snapshot(); - steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); - steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME); - steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); - - var response = steps.deleteSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); - - assertThat(response.isAcknowledged(), equalTo(true)); - assertThat(internalClient, snapshotInClusterDoesNotExists(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/delete-snapshot-positive")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(DELETE, "/_snapshot/test-snapshot-repository/delete-snapshot-positive")); - auditLogsRule.assertAtLeast(1, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/delete-snapshot-positive")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "DeleteSnapshotRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); - } - - @Test - public void shouldDeleteSnapshot_negative() throws IOException { - String snapshotName = "delete-snapshot-negative"; - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); - steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); - steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME); - steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); - } - try(RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); - assertThatThrownBy(() -> steps.deleteSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName), statusException(FORBIDDEN)); - - assertThat(internalClient, clusterContainSuccessSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/delete-snapshot-negative")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(DELETE, "/_snapshot/test-snapshot-repository/delete-snapshot-negative")); - auditLogsRule.assertAtLeast(1, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/delete-snapshot-negative")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); - auditLogsRule.assertExactly(1, missingPrivilege(LIMITED_READ_USER, "DeleteSnapshotRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); - } - - @Test - public void shouldRestoreSnapshot_positive() throws IOException { - final String snapshotName = "restore-snapshot-positive"; - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); - // 1. create some documents - BulkRequest bulkRequest = new BulkRequest(); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Eins").source(SONGS[0].asMap())); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Zwei").source(SONGS[1].asMap())); - bulkRequest.setRefreshPolicy(IMMEDIATE); - restHighLevelClient.bulk(bulkRequest, DEFAULT); - - //2. create snapshot repository - steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); - - // 3. create snapshot - steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, WRITE_SONG_INDEX_NAME) ; - - // 4. wait till snapshot is ready - steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); - - // 5. introduce some changes - bulkRequest = new BulkRequest(); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Drei").source(SONGS[2].asMap())); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Vier").source(SONGS[3].asMap())); - bulkRequest.add(new DeleteRequest(WRITE_SONG_INDEX_NAME, "Eins")); - bulkRequest.setRefreshPolicy(IMMEDIATE); - restHighLevelClient.bulk(bulkRequest, DEFAULT); - - // 6. restore the snapshot - var response = steps.restoreSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, "(.+)", "restored_$1"); - - assertThat(response, notNullValue()); - assertThat(response.status(), equalTo(ACCEPTED)); - - // 7. wait until snapshot is restored - CountRequest countRequest = new CountRequest(RESTORED_SONG_INDEX_NAME); - Awaitility.await().ignoreExceptions().alias("Index contains proper number of documents restored from snapshot.") - .until(() -> restHighLevelClient.count(countRequest, DEFAULT).getCount() == 2); - - //8. verify that document are present in restored index - assertThat(internalClient, clusterContainsDocumentWithFieldValue(RESTORED_SONG_INDEX_NAME, "Eins", FIELD_TITLE, TITLE_MAGNUM_OPUS)); - assertThat(internalClient, clusterContainsDocumentWithFieldValue(RESTORED_SONG_INDEX_NAME, "Zwei", FIELD_TITLE, TITLE_SONG_1_PLUS_1)); - assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Drei"))); - assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Vier"))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/restore-snapshot-positive")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_snapshot/test-snapshot-repository/restore-snapshot-positive/_restore")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/restored_write_song_index/_count")); - auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertAtLeast(1, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/restore-snapshot-positive")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "RestoreSnapshotRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "SearchRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - } - - @Test - public void shouldRestoreSnapshot_failureForbiddenIndex() throws IOException { - final String snapshotName = "restore-snapshot-negative-forbidden-index"; - String restoreToIndex = "forbidden_index"; - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); - // 1. create some documents - BulkRequest bulkRequest = new BulkRequest(); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Eins").source(SONGS[0].asMap())); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Zwei").source(SONGS[1].asMap())); - bulkRequest.setRefreshPolicy(IMMEDIATE); - restHighLevelClient.bulk(bulkRequest, DEFAULT); - - //2. create snapshot repository - steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); - - // 3. create snapshot - steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, WRITE_SONG_INDEX_NAME); - - // 4. wait till snapshot is ready - steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); - - // 5. restore the snapshot - assertThatThrownBy(() -> steps.restoreSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, "(.+)", restoreToIndex), - statusException(FORBIDDEN)); - - - //6. verify that document are not present in restored index - assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Eins"))); - assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Zwei"))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-index")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-index/_restore")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertAtLeast(1, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-index")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "RestoreSnapshotRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "RestoreSnapshotRequest")); - } - - @Test - public void shouldRestoreSnapshot_failureOperationForbidden() throws IOException { - String snapshotName = "restore-snapshot-negative-forbidden-operation"; - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { - SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); - // 1. create some documents - BulkRequest bulkRequest = new BulkRequest(); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Eins").source(SONGS[0].asMap())); - bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Zwei").source(SONGS[1].asMap())); - bulkRequest.setRefreshPolicy(IMMEDIATE); - restHighLevelClient.bulk(bulkRequest, DEFAULT); - - //2. create snapshot repository - steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); - - // 3. create snapshot - steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, WRITE_SONG_INDEX_NAME); - - // 4. wait till snapshot is ready - steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); - } - // 5. restore the snapshot - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { - SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); - assertThatThrownBy( () -> steps.restoreSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, "(.+)", "restored_$1"), - statusException(FORBIDDEN)); - - // 6. verify that documents does not exist - assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Eins"))); - assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Zwei"))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-operation")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-operation/_restore")); - auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); - auditLogsRule.assertAtLeast(1, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-operation")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); - auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); - auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); - auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); - auditLogsRule.assertExactly(1, missingPrivilege(LIMITED_READ_USER, "RestoreSnapshotRequest")); - auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); - auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); - } - - @Test - //required permissions: "indices:admin/create" - public void createIndex_positive() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_positive"); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName); - CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest, DEFAULT); - - assertThat(createIndexResponse, isSuccessfulCreateIndexResponse(indexName)); - assertThat(cluster, indexExists(indexName)); - } - } - - @Test - public void createIndex_negative() throws IOException { - String indexName = "create_index_negative"; - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName); - - assertThatThrownBy(() -> restHighLevelClient.indices().create(createIndexRequest, DEFAULT), statusException(FORBIDDEN)); - assertThat(cluster, not(indexExists(indexName))); - } - } - - @Test - //required permissions: "indices:admin/get" - public void checkIfIndexExists_positive() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("index_exists_positive"); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - boolean exists = restHighLevelClient.indices().exists(new GetIndexRequest(indexName), DEFAULT); - - assertThat(exists, is(false)); - } - } - - @Test - public void checkIfIndexExists_negative() throws IOException { - String indexThatUserHasNoAccessTo = "index_exists_negative"; - String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - assertThatThrownBy(() -> - restHighLevelClient.indices().exists(new GetIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().exists(new GetIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().exists(new GetIndexRequest("*"), DEFAULT), - statusException(FORBIDDEN) - ); - } - } - - @Test - //required permissions: "indices:admin/delete" - public void deleteIndex_positive() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("delete_index_positive"); - IndexOperationsHelper.createIndex(cluster, indexName); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(indexName); - var response = restHighLevelClient.indices().delete(deleteIndexRequest, DEFAULT); - - assertThat(response.isAcknowledged(), is(true)); - assertThat(cluster, not(indexExists(indexName))); - } - } - - @Test - public void deleteIndex_negative() throws IOException { - String indexThatUserHasNoAccessTo = "delete_index_negative"; - String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - - assertThatThrownBy(() -> - restHighLevelClient.indices().delete(new DeleteIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().delete(new DeleteIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().delete(new DeleteIndexRequest("*"), DEFAULT), statusException(FORBIDDEN) - ); - } - } - - @Test - //required permissions: indices:admin/aliases, indices:admin/delete - public void shouldDeleteIndexByAliasRequest_positive() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("delete_index_by_alias_request_positive"); - IndexOperationsHelper.createIndex(cluster, indexName); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - IndicesAliasesRequest request = new IndicesAliasesRequest().addAliasAction(new AliasActions(REMOVE_INDEX).indices(indexName)); - - var response = restHighLevelClient.indices().updateAliases(request, DEFAULT); - - assertThat(response.isAcknowledged(), is(true)); - assertThat(cluster, not(indexExists(indexName))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES).withRestRequest(POST, "/_aliases")); - auditLogsRule.assertExactly(2, grantedPrivilege(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, "IndicesAliasesRequest")); - auditLogsRule.assertExactly(2, auditPredicate(INDEX_EVENT).withEffectiveUser(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)); - } - - @Test - public void shouldDeleteIndexByAliasRequest_negative() throws IOException { - String indexName = "delete_index_by_alias_request_negative"; - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - IndicesAliasesRequest request = new IndicesAliasesRequest().addAliasAction(new AliasActions(REMOVE_INDEX).indices(indexName)); - - assertThatThrownBy(() -> restHighLevelClient.indices().updateAliases(request, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - //required permissions: "indices:admin/get" - public void getIndex_positive() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("get_index_positive"); - IndexOperationsHelper.createIndex(cluster, indexName); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - GetIndexRequest getIndexRequest = new GetIndexRequest(indexName); - GetIndexResponse response = restHighLevelClient.indices().get(getIndexRequest, DEFAULT); - - assertThat(response, getIndexResponseContainsIndices(indexName)); - } - } - - @Test - public void getIndex_negative() throws IOException { - String indexThatUserHasNoAccessTo = "get_index_negative"; - String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - - assertThatThrownBy(() -> - restHighLevelClient.indices().get(new GetIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().get(new GetIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().get(new GetIndexRequest("*"), DEFAULT), - statusException(FORBIDDEN) - ); - } - } - - @Test - //required permissions: "indices:admin/close", "indices:admin/close*" - public void closeIndex_positive() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("close_index_positive"); - IndexOperationsHelper.createIndex(cluster, indexName); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - CloseIndexRequest closeIndexRequest = new CloseIndexRequest(indexName); - CloseIndexResponse response = restHighLevelClient.indices().close(closeIndexRequest, DEFAULT); - - assertThat(response, isSuccessfulCloseIndexResponse()); - assertThat(cluster, indexStateIsEqualTo(indexName, IndexMetadata.State.CLOSE)); - } - } - - @Test - public void closeIndex_negative() throws IOException { - String indexThatUserHasNoAccessTo = "close_index_negative"; - String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - - assertThatThrownBy(() -> - restHighLevelClient.indices().close(new CloseIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().close(new CloseIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().close(new CloseIndexRequest("*"), DEFAULT), - statusException(FORBIDDEN) - ); - } - } - - @Test - //required permissions: "indices:admin/open" - public void openIndex_positive() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("open_index_positive"); - IndexOperationsHelper.createIndex(cluster, indexName); - IndexOperationsHelper.closeIndex(cluster, indexName); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - OpenIndexRequest closeIndexRequest = new OpenIndexRequest(indexName); - OpenIndexResponse response = restHighLevelClient.indices().open(closeIndexRequest, DEFAULT); - - assertThat(response, isSuccessfulOpenIndexResponse()); - assertThat(cluster, indexStateIsEqualTo(indexName, IndexMetadata.State.OPEN)); - } - } - - @Test - public void openIndex_negative() throws IOException { - String indexThatUserHasNoAccessTo = "open_index_negative"; - String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - - assertThatThrownBy(() -> - restHighLevelClient.indices().open(new OpenIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().open(new OpenIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().open(new OpenIndexRequest("*"), DEFAULT), - statusException(FORBIDDEN) - ); - } - } - - @Test - @Ignore - //required permissions: "indices:admin/resize", "indices:monitor/stats - // todo even when I assign the `indices:admin/resize` and `indices:monitor/stats` permissions to test user, this test fails. - // Issue: https://github.com/opensearch-project/security/issues/2141 - public void shrinkIndex_positive() throws IOException { - String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_positive_source"); - Settings sourceIndexSettings = Settings.builder() - .put("index.blocks.write", true) - .put("index.number_of_shards", 2) - .build(); - String targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_positive_target"); - IndexOperationsHelper.createIndex(cluster, sourceIndexName, sourceIndexSettings); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); - ResizeResponse response = restHighLevelClient.indices().shrink(resizeRequest, DEFAULT); - - assertThat(response, isSuccessfulResizeResponse(targetIndexName)); - assertThat(cluster, indexExists(targetIndexName)); - } - } - - @Test - public void shrinkIndex_negative() throws IOException { - //user cannot access target index - String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_negative_source"); - String targetIndexName = "shrink_index_negative_target"; - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); - - assertThatThrownBy(() -> restHighLevelClient.indices().shrink(resizeRequest, DEFAULT), statusException(FORBIDDEN)); - assertThat(cluster, not(indexExists(targetIndexName))); - } - - //user cannot access source index - sourceIndexName = "shrink_index_negative_source"; - targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_negative_target"); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); - - assertThatThrownBy(() -> restHighLevelClient.indices().shrink(resizeRequest, DEFAULT), statusException(FORBIDDEN)); - assertThat(cluster, not(indexExists(targetIndexName))); - } - } - - @Test - @Ignore - //required permissions: "indices:admin/resize", "indices:monitor/stats - // todo even when I assign the `indices:admin/resize` and `indices:monitor/stats` permissions to test user, this test fails. - // Issue: https://github.com/opensearch-project/security/issues/2141 - public void cloneIndex_positive() throws IOException { - String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clone_index_positive_source"); - Settings sourceIndexSettings = Settings.builder() - .put("index.blocks.write", true) - .build(); - String targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clone_index_positive_target"); - IndexOperationsHelper.createIndex(cluster, sourceIndexName, sourceIndexSettings); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); - ResizeResponse response = restHighLevelClient.indices().clone(resizeRequest, DEFAULT); - - assertThat(response, isSuccessfulResizeResponse(targetIndexName)); - assertThat(cluster, indexExists(targetIndexName)); - } - } - - @Test - public void cloneIndex_negative() throws IOException { - //user cannot access target index - String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clone_index_negative_source"); - String targetIndexName = "clone_index_negative_target"; - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); - - assertThatThrownBy(() -> restHighLevelClient.indices().clone(resizeRequest, DEFAULT), statusException(FORBIDDEN)); - assertThat(cluster, not(indexExists(targetIndexName))); - } - - //user cannot access source index - sourceIndexName = "clone_index_negative_source"; - targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clone_index_negative_target"); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); - - assertThatThrownBy(() -> restHighLevelClient.indices().clone(resizeRequest, DEFAULT), statusException(FORBIDDEN)); - assertThat(cluster, not(indexExists(targetIndexName))); - } - } - - @Test - @Ignore - //required permissions: "indices:admin/resize", "indices:monitor/stats - // todo even when I assign the `indices:admin/resize` and `indices:monitor/stats` permissions to test user, this test fails. - // Issue: https://github.com/opensearch-project/security/issues/2141 - public void splitIndex_positive() throws IOException { - String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("split_index_positive_source"); - Settings sourceIndexSettings = Settings.builder() - .put("index.blocks.write", true) - .build(); - String targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("split_index_positive_target"); - IndexOperationsHelper.createIndex(cluster, sourceIndexName, sourceIndexSettings); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); - resizeRequest.setSettings(Settings.builder().put("index.number_of_shards", 2).build()); - ResizeResponse response = restHighLevelClient.indices().split(resizeRequest, DEFAULT); - - assertThat(response, isSuccessfulResizeResponse(targetIndexName)); - assertThat(cluster, indexExists(targetIndexName)); - } - } - - @Test - public void splitIndex_negative() throws IOException { - //user cannot access target index - String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("split_index_negative_source"); - String targetIndexName = "split_index_negative_target"; - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); - resizeRequest.setSettings(Settings.builder().put("index.number_of_shards", 2).build()); - - assertThatThrownBy(() -> restHighLevelClient.indices().split(resizeRequest, DEFAULT), statusException(FORBIDDEN)); - assertThat(cluster, not(indexExists(targetIndexName))); - } - - //user cannot access source index - sourceIndexName = "split_index_negative_source"; - targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("split_index_negative_target"); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); - resizeRequest.setSettings(Settings.builder().put("index.number_of_shards", 2).build()); - - assertThatThrownBy(() -> restHighLevelClient.indices().split(resizeRequest, DEFAULT), statusException(FORBIDDEN)); - assertThat(cluster, not(indexExists(targetIndexName))); - } - } - - @Test - //required permissions: "indices:monitor/settings/get" - public void getIndexSettings_positive() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("get_index_settings_positive"); - IndexOperationsHelper.createIndex(cluster, indexName); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - GetSettingsRequest getSettingsRequest = new GetSettingsRequest().indices(indexName); - GetSettingsResponse response = restHighLevelClient.indices().getSettings(getSettingsRequest, DEFAULT); - - assertThat(response, getSettingsResponseContainsIndices(indexName)); - } - } - - @Test - public void getIndexSettings_negative() throws IOException { - String indexThatUserHasNoAccessTo = "get_index_settings_negative"; - String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - assertThatThrownBy(() -> - restHighLevelClient.indices().getSettings(new GetSettingsRequest().indices(indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().getSettings(new GetSettingsRequest().indices(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().getSettings(new GetSettingsRequest().indices("*"), DEFAULT), - statusException(FORBIDDEN) - ); - } - } - - @Test - //required permissions: "indices:admin/settings/update" - public void updateIndexSettings_positive() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("update_index_settings_positive"); - Settings initialSettings = Settings.builder().put("index.number_of_replicas", "2").build(); - Settings updatedSettings = Settings.builder().put("index.number_of_replicas", "4").build(); - IndexOperationsHelper.createIndex(cluster, indexName, initialSettings); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - UpdateSettingsRequest updateSettingsRequest = new UpdateSettingsRequest(indexName) - .settings(updatedSettings); - var response = restHighLevelClient.indices().putSettings(updateSettingsRequest, DEFAULT); - - assertThat(response.isAcknowledged(), is(true)); - assertThat(cluster, indexSettingsContainValues(indexName, updatedSettings)); - } - } - - @Test - public void updateIndexSettings_negative() throws IOException { - String indexThatUserHasNoAccessTo = "update_index_settings_negative"; - String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); - Settings settingsToUpdate = Settings.builder().put("index.number_of_replicas", 2).build(); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - - assertThatThrownBy(() -> - restHighLevelClient.indices().putSettings(new UpdateSettingsRequest(indexThatUserHasNoAccessTo).settings(settingsToUpdate), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().putSettings(new UpdateSettingsRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo).settings(settingsToUpdate), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().putSettings(new UpdateSettingsRequest("*").settings(settingsToUpdate), DEFAULT), - statusException(FORBIDDEN) - ); - } - } - - @Test - //required permissions: indices:admin/mapping/put - public void createIndexMappings_positive() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_mappings_positive"); - Map indexMapping = Map.of("properties", Map.of("message", Map.of("type", "text"))); - IndexOperationsHelper.createIndex(cluster, indexName); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - PutMappingRequest putMappingRequest = new PutMappingRequest(indexName).source(indexMapping); - var response = restHighLevelClient.indices().putMapping(putMappingRequest, DEFAULT); - - assertThat(response.isAcknowledged(), is(true)); - assertThat(cluster, indexMappingIsEqualTo(indexName, indexMapping)); - } - } - - @Test - public void createIndexMappings_negative() throws IOException { - String indexThatUserHasNoAccessTo = "create_index_mappings_negative"; - String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); - Map indexMapping = Map.of("properties", Map.of("message", Map.of("type", "text"))); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - - assertThatThrownBy(() -> - restHighLevelClient.indices().putMapping(new PutMappingRequest(indexThatUserHasNoAccessTo).source(indexMapping), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().putMapping(new PutMappingRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo).source(indexMapping), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().putMapping(new PutMappingRequest("*").source(indexMapping), DEFAULT), - statusException(FORBIDDEN) - ); - } - } - - @Test - //required permissions: indices:admin/mappings/get - public void getIndexMappings_positive() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("get_index_mappings_positive"); - Map indexMapping = Map.of("properties", Map.of("message", Map.of("type", "text"))); - IndexOperationsHelper.createIndex(cluster, indexName); - IndexOperationsHelper.createMapping(cluster, indexName, indexMapping); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - GetMappingsRequest getMappingsRequest = new GetMappingsRequest().indices(indexName); - GetMappingsResponse response = restHighLevelClient.indices().getMapping(getMappingsRequest, DEFAULT); - - assertThat(response, getMappingsResponseContainsIndices(indexName)); - } - } - - @Test - public void getIndexMappings_negative() throws IOException { - String indexThatUserHasNoAccessTo = "get_index_mappings_negative"; - String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - - assertThatThrownBy(() -> - restHighLevelClient.indices().getMapping(new GetMappingsRequest().indices(indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().getMapping(new GetMappingsRequest().indices(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().getMapping(new GetMappingsRequest().indices("*"), DEFAULT), - statusException(FORBIDDEN) - ); - } - } - - @Test - //required permissions: "indices:admin/cache/clear" - public void clearIndexCache_positive() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clear_index_cache_positive"); - IndexOperationsHelper.createIndex(cluster, indexName); - - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - ClearIndicesCacheRequest clearIndicesCacheRequest = new ClearIndicesCacheRequest(indexName); - ClearIndicesCacheResponse response = restHighLevelClient.indices().clearCache(clearIndicesCacheRequest, DEFAULT); - - assertThat(response, isSuccessfulClearIndicesCacheResponse()); - } - } - - @Test - public void clearIndexCache_negative() throws IOException { - String indexThatUserHasNoAccessTo = "clear_index_cache_negative"; - String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - - assertThatThrownBy(() -> - restHighLevelClient.indices().clearCache(new ClearIndicesCacheRequest(indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().clearCache(new ClearIndicesCacheRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), - statusException(FORBIDDEN) - ); - assertThatThrownBy(() -> - restHighLevelClient.indices().clearCache(new ClearIndicesCacheRequest("*"), DEFAULT), - statusException(FORBIDDEN) - ); - } - } - - @Test - //required permissions: "indices:admin/create", "indices:admin/aliases" - public void shouldCreateIndexWithAlias_positive() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_with_alias_positive"); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)) { - CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName) - .alias(new Alias(ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE)); - - CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest, DEFAULT); - - assertThat(createIndexResponse, isSuccessfulCreateIndexResponse(indexName)); - assertThat(cluster, indexExists(indexName)); - assertThat(internalClient, aliasExists(ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE)); - } - auditLogsRule.assertExactlyOne(userAuthenticated(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES).withRestRequest(PUT, "/index_operations_create_index_with_alias_positive")); - auditLogsRule.assertExactly(2, grantedPrivilege(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, "CreateIndexRequest")); - auditLogsRule.assertExactly(2, auditPredicate(INDEX_EVENT).withEffectiveUser(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES)); - } - - @Test - public void shouldCreateIndexWithAlias_negative() throws IOException { - String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_with_alias_negative"); - try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_CREATE_INDEX)) { - CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName) - .alias(new Alias(ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE)); - - assertThatThrownBy(() -> restHighLevelClient.indices().create(createIndexRequest, DEFAULT), statusException(FORBIDDEN)); - - assertThat(internalClient, not(aliasExists(ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE))); - } - auditLogsRule.assertExactlyOne(userAuthenticated(USER_ALLOWED_TO_CREATE_INDEX).withRestRequest(PUT, "/index_operations_create_index_with_alias_negative")); - auditLogsRule.assertExactlyOne(missingPrivilege(USER_ALLOWED_TO_CREATE_INDEX, "CreateIndexRequest")); - } + private static final Logger log = LogManager.getLogger(SearchOperationTest.class); + + public static final String SONG_INDEX_NAME = "song_lyrics"; + public static final String PROHIBITED_SONG_INDEX_NAME = "prohibited_song_lyrics"; + public static final String WRITE_SONG_INDEX_NAME = "write_song_index"; + + public static final String SONG_LYRICS_ALIAS = "song_lyrics_index_alias"; + public static final String PROHIBITED_SONG_ALIAS = "prohibited_song_lyrics_index_alias"; + private static final String COLLECTIVE_INDEX_ALIAS = "collective-index-alias"; + private static final String TEMPLATE_INDEX_PREFIX = "song-transcription*"; + public static final String TEMPORARY_ALIAS_NAME = "temporary-alias"; + public static final String ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001 = "alias-used-in-musical-index-template-0001"; + public static final String ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002 = "alias-used-in-musical-index-template-0002"; + public static final String ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003 = "alias-used-in-musical-index-template-0003"; + public static final String INDEX_NAME_SONG_TRANSCRIPTION_JAZZ = "song-transcription-jazz"; + + public static final String MUSICAL_INDEX_TEMPLATE = "musical-index-template"; + public static final String ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE = "alias_create_index_with_alias_positive"; + public static final String ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE = "alias_create_index_with_alias_negative"; + + public static final String UNDELETABLE_TEMPLATE_NAME = "undeletable-template-name"; + + public static final String ALIAS_FROM_UNDELETABLE_TEMPLATE = "alias-from-undeletable-template"; + + public static final String TEST_SNAPSHOT_REPOSITORY_NAME = "test-snapshot-repository"; + + public static final String UNUSED_SNAPSHOT_REPOSITORY_NAME = "unused-snapshot-repository"; + + public static final String RESTORED_SONG_INDEX_NAME = "restored_" + WRITE_SONG_INDEX_NAME; + + public static final String UPDATE_DELETE_OPERATION_INDEX_NAME = "update_delete_index"; + + public static final String DOCUMENT_TO_UPDATE_ID = "doc_to_update"; + + private static final String ID_P4 = "4"; + private static final String ID_S3 = "3"; + private static final String ID_S2 = "2"; + private static final String ID_S1 = "1"; + + static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS); + + /** + * All user read permissions are related to {@link #SONG_INDEX_NAME} index + */ + static final User LIMITED_READ_USER = new User("limited_read_user").roles( + new Role("limited-song-reader").clusterPermissions( + "indices:data/read/mget", + "indices:data/read/msearch", + "indices:data/read/scroll" + ) + .indexPermissions( + "indices:data/read/search", + "indices:data/read/get", + "indices:data/read/mget*", + "indices:admin/aliases", + "indices:data/read/field_caps", + "indices:data/read/field_caps*" + ) + .on(SONG_INDEX_NAME) + ); + + static final User LIMITED_WRITE_USER = new User("limited_write_user").roles( + new Role("limited-write-role").clusterPermissions( + "indices:data/write/bulk", + "indices:admin/template/put", + "indices:admin/template/delete", + "cluster:admin/repository/put", + "cluster:admin/repository/delete", + "cluster:admin/snapshot/create", + "cluster:admin/snapshot/status", + "cluster:admin/snapshot/status[nodes]", + "cluster:admin/snapshot/delete", + "cluster:admin/snapshot/get", + "cluster:admin/snapshot/restore" + ) + .indexPermissions( + "indices:data/write/index", + "indices:data/write/bulk[s]", + "indices:admin/create", + "indices:admin/mapping/put", + "indices:data/write/update", + "indices:data/write/bulk[s]", + "indices:data/write/delete", + "indices:data/write/bulk[s]" + ) + .on(WRITE_SONG_INDEX_NAME), + new Role("transcription-role").indexPermissions( + "indices:data/write/index", + "indices:admin/create", + "indices:data/write/bulk[s]", + "indices:admin/mapping/put" + ).on(INDEX_NAME_SONG_TRANSCRIPTION_JAZZ), + new Role("limited-write-index-restore-role").indexPermissions( + "indices:data/write/index", + "indices:admin/create", + "indices:data/read/search" + ).on(RESTORED_SONG_INDEX_NAME) + ); + + /** + * User who is allowed read both index {@link #SONG_INDEX_NAME} and {@link #PROHIBITED_SONG_INDEX_NAME} + */ + static final User DOUBLE_READER_USER = new User("double_read_user").roles( + new Role("full-song-reader").indexPermissions("indices:data/read/search").on(SONG_INDEX_NAME, PROHIBITED_SONG_INDEX_NAME) + ); + + static final User REINDEXING_USER = new User("reindexing_user").roles( + new Role("song-reindexing-target-write").clusterPermissions("indices:data/write/reindex", "indices:data/write/bulk") + .indexPermissions("indices:admin/create", "indices:data/write/index", "indices:data/write/bulk[s]", "indices:admin/mapping/put") + .on(WRITE_SONG_INDEX_NAME), + new Role("song-reindexing-source-read").clusterPermissions("indices:data/read/scroll") + .indexPermissions("indices:data/read/search") + .on(SONG_INDEX_NAME) + ); + + private Client internalClient; + /** + * User who is allowed to update and delete documents on index {@link #UPDATE_DELETE_OPERATION_INDEX_NAME} + */ + static final User UPDATE_DELETE_USER = new User("update_delete_user").roles( + new Role("document-updater").clusterPermissions("indices:data/write/bulk") + .indexPermissions( + "indices:data/write/update", + "indices:data/write/index", + "indices:data/write/bulk[s]", + "indices:admin/mapping/put" + ) + .on(UPDATE_DELETE_OPERATION_INDEX_NAME), + new Role("document-remover").indexPermissions("indices:data/write/delete").on(UPDATE_DELETE_OPERATION_INDEX_NAME) + ); + + static final String INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX = "index_operations_"; + + /** + * User who is allowed to perform index-related operations on + * indices with names prefixed by the {@link #INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX} + */ + static final User USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES = new User("index-operation-tester").roles( + new Role("index-manager").indexPermissions( + "indices:admin/create", + "indices:admin/get", + "indices:admin/delete", + "indices:admin/close", + "indices:admin/close*", + "indices:admin/open", + "indices:admin/resize", + "indices:monitor/stats", + "indices:monitor/settings/get", + "indices:admin/settings/update", + "indices:admin/mapping/put", + "indices:admin/mappings/get", + "indices:admin/cache/clear", + "indices:admin/aliases" + ).on(INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("*")) + ); + + private static User USER_ALLOWED_TO_CREATE_INDEX = new User("user-allowed-to-create-index").roles( + new Role("create-index-role").indexPermissions("indices:admin/create").on("*") + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .anonymousAuth(false) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users( + ADMIN_USER, + LIMITED_READ_USER, + LIMITED_WRITE_USER, + DOUBLE_READER_USER, + REINDEXING_USER, + UPDATE_DELETE_USER, + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, + USER_ALLOWED_TO_CREATE_INDEX + ) + .audit( + new AuditConfiguration(true).compliance(new AuditCompliance().enabled(true)) + .filters(new AuditFilters().enabledRest(true).enabledTransport(true)) + ) + .build(); + + @Rule + public AuditLogsRule auditLogsRule = new AuditLogsRule(); + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + client.prepareIndex(SONG_INDEX_NAME).setId(ID_S1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); + client.prepareIndex(UPDATE_DELETE_OPERATION_INDEX_NAME) + .setId(DOCUMENT_TO_UPDATE_ID) + .setRefreshPolicy(IMMEDIATE) + .setSource("field", "value") + .get(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction(new AliasActions(ADD).indices(SONG_INDEX_NAME).alias(SONG_LYRICS_ALIAS)) + ) + .actionGet(); + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(SONG_INDEX_NAME).id(ID_S2).source(SONGS[1].asMap())) + .actionGet(); + client.index(new IndexRequest().setRefreshPolicy(IMMEDIATE).index(SONG_INDEX_NAME).id(ID_S3).source(SONGS[2].asMap())) + .actionGet(); + + client.prepareIndex(PROHIBITED_SONG_INDEX_NAME).setId(ID_P4).setSource(SONGS[3].asMap()).setRefreshPolicy(IMMEDIATE).get(); + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new AliasActions(ADD).indices(PROHIBITED_SONG_INDEX_NAME).alias(PROHIBITED_SONG_ALIAS) + ) + ) + .actionGet(); + + client.admin() + .indices() + .aliases( + new IndicesAliasesRequest().addAliasAction( + new AliasActions(ADD).indices(SONG_INDEX_NAME, PROHIBITED_SONG_INDEX_NAME).alias(COLLECTIVE_INDEX_ALIAS) + ) + ) + .actionGet(); + var createTemplateRequest = new org.opensearch.action.admin.indices.template.put.PutIndexTemplateRequest( + UNDELETABLE_TEMPLATE_NAME + ); + createTemplateRequest.patterns(List.of("pattern-does-not-match-to-any-index")); + createTemplateRequest.alias(new Alias(ALIAS_FROM_UNDELETABLE_TEMPLATE)); + client.admin().indices().putTemplate(createTemplateRequest).actionGet(); + + client.admin() + .cluster() + .putRepository( + new PutRepositoryRequest(UNUSED_SNAPSHOT_REPOSITORY_NAME).type("fs") + .settings(Map.of("location", cluster.getSnapshotDirPath())) + ) + .actionGet(); + } + } + + @Before + public void retrieveClusterClient() { + this.internalClient = cluster.getInternalNodeClient(); + } + + @After + public void cleanData() throws ExecutionException, InterruptedException { + Stopwatch stopwatch = Stopwatch.createStarted(); + IndicesAdminClient indices = internalClient.admin().indices(); + List indicesToBeDeleted = List.of( + WRITE_SONG_INDEX_NAME, + INDEX_NAME_SONG_TRANSCRIPTION_JAZZ, + RESTORED_SONG_INDEX_NAME, + INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("*") + ); + for (String indexToBeDeleted : indicesToBeDeleted) { + IndicesExistsRequest indicesExistsRequest = new IndicesExistsRequest(indexToBeDeleted); + var indicesExistsResponse = indices.exists(indicesExistsRequest).get(); + if (indicesExistsResponse.isExists()) { + DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(indexToBeDeleted); + indices.delete(deleteIndexRequest).actionGet(); + Awaitility.await().ignoreExceptions().until(() -> indices.exists(indicesExistsRequest).get().isExists() == false); + } + } + + List aliasesToBeDeleted = List.of( + TEMPORARY_ALIAS_NAME, + ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001, + ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002, + ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE, + ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE + ); + for (String aliasToBeDeleted : aliasesToBeDeleted) { + if (indices.exists(new IndicesExistsRequest(aliasToBeDeleted)).get().isExists()) { + AliasActions aliasAction = new AliasActions(AliasActions.Type.REMOVE).indices(SONG_INDEX_NAME).alias(aliasToBeDeleted); + internalClient.admin().indices().aliases(new IndicesAliasesRequest().addAliasAction(aliasAction)).get(); + } + } + + GetIndexTemplatesResponse response = indices.getTemplates(new GetIndexTemplatesRequest(MUSICAL_INDEX_TEMPLATE)).get(); + for (IndexTemplateMetadata metadata : response.getIndexTemplates()) { + indices.deleteTemplate(new DeleteIndexTemplateRequest(metadata.getName())).get(); + } + + ClusterAdminClient clusterClient = internalClient.admin().cluster(); + try { + clusterClient.deleteRepository(new DeleteRepositoryRequest(TEST_SNAPSHOT_REPOSITORY_NAME)).actionGet(); + } catch (RepositoryMissingException e) { + log.debug("Repository '{}' does not exist. This is expected in most of test cases", TEST_SNAPSHOT_REPOSITORY_NAME, e); + } + internalClient.close(); + log.debug("Cleaning data after test took {}", stopwatch.stop()); + } + + @Test + public void shouldSearchForDocuments_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldSearchForDocuments_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(PROHIBITED_SONG_INDEX_NAME, QUERY_TITLE_POISON); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldSearchForDocumentsViaAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(SONG_LYRICS_ALIAS, QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics_index_alias/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldSearchForDocumentsViaAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(PROHIBITED_SONG_ALIAS, QUERY_TITLE_POISON); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics_index_alias/_search") + ); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchSongViaMultiIndexAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(COLLECTIVE_INDEX_ALIAS, QUERY_TITLE_NEXT_SONG); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S3)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_NEXT_SONG)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/collective-index-alias/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(DOUBLE_READER_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchSongViaMultiIndexAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(COLLECTIVE_INDEX_ALIAS, QUERY_TITLE_POISON); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/collective-index-alias/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchAllIndexes_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(ADMIN_USER).withRestRequest(POST, "/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(ADMIN_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchAllIndexes_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest(QUERY_TITLE_MAGNUM_OPUS); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchSongIndexesWithAsterisk_prohibitedSongIndex_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("*" + SONG_INDEX_NAME, QUERY_TITLE_POISON); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, PROHIBITED_SONG_INDEX_NAME, ID_P4)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_POISON)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/*song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(DOUBLE_READER_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchSongIndexesWithAsterisk_singIndex_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("*" + SONG_INDEX_NAME, QUERY_TITLE_NEXT_SONG); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S3)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_NEXT_SONG)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/*song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(DOUBLE_READER_USER, "SearchRequest")); + } + + @Test + public void shouldBeAbleToSearchSongIndexesWithAsterisk_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("*" + SONG_INDEX_NAME, QUERY_TITLE_NEXT_SONG); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/*song_lyrics/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldFindSongUsingDslQuery_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = new SearchRequest(SONG_INDEX_NAME); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); + boolQueryBuilder.filter(QueryBuilders.regexpQuery(FIELD_ARTIST, "f.+")); + boolQueryBuilder.filter(new MatchQueryBuilder(FIELD_TITLE, TITLE_MAGNUM_OPUS)); + searchSourceBuilder.query(boolQueryBuilder); + searchRequest.source(searchSourceBuilder); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldFindSongUsingDslQuery_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = new SearchRequest(PROHIBITED_SONG_INDEX_NAME); + SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); + BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery(); + boolQueryBuilder.filter(QueryBuilders.regexpQuery(FIELD_ARTIST, "n.+")); + boolQueryBuilder.filter(new MatchQueryBuilder(FIELD_TITLE, TITLE_POISON)); + searchSourceBuilder.query(boolQueryBuilder); + searchRequest.source(searchSourceBuilder); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldPerformSearchWithAllIndexAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("_all", QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); + assertThat(searchResponse, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(ADMIN_USER).withRestRequest(POST, "/_all/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(ADMIN_USER, "SearchRequest")); + } + + @Test + public void shouldPerformSearchWithAllIndexAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = queryStringQueryRequest("_all", QUERY_TITLE_MAGNUM_OPUS); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_all/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldScrollOverSearchResults_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = searchRequestWithScroll(SONG_INDEX_NAME, 2); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containNotEmptyScrollingId()); + + SearchScrollRequest scrollRequest = getSearchScrollRequest(searchResponse); + + SearchResponse scrollResponse = restHighLevelClient.scroll(scrollRequest, DEFAULT); + assertThat(scrollResponse, isSuccessfulSearchResponse()); + assertThat(scrollResponse, containNotEmptyScrollingId()); + assertThat(scrollResponse, numberOfTotalHitsIsEqualTo(3)); + assertThat(scrollResponse, numberOfHitsInPageIsEqualTo(1)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_search/scroll")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchScrollRequest")); + } + + @Test + public void shouldScrollOverSearchResults_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { + SearchRequest searchRequest = searchRequestWithScroll(SONG_INDEX_NAME, 2); + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containNotEmptyScrollingId()); + + SearchScrollRequest scrollRequest = getSearchScrollRequest(searchResponse); + + assertThatThrownBy(() -> restHighLevelClient.scroll(scrollRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(DOUBLE_READER_USER, "SearchRequest")); + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/_search/scroll")); + auditLogsRule.assertExactlyOne(missingPrivilege(DOUBLE_READER_USER, "SearchScrollRequest")); + } + + @Test + public void shouldGetDocument_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + GetResponse response = restHighLevelClient.get(new GetRequest(SONG_INDEX_NAME, ID_S1), DEFAULT); + + assertThat(response, containDocument(SONG_INDEX_NAME, ID_S1)); + assertThat(response, documentContainField(FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/song_lyrics/_doc/1")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "GetRequest")); + } + + @Test + public void shouldGetDocument_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + GetRequest getRequest = new GetRequest(PROHIBITED_SONG_INDEX_NAME, ID_P4); + assertThatThrownBy(() -> restHighLevelClient.get(getRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/prohibited_song_lyrics/_doc/4")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "GetRequest")); + } + + @Test + public void shouldPerformMultiGetDocuments_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + MultiGetRequest request = new MultiGetRequest(); + request.add(new Item(SONG_INDEX_NAME, ID_S1)); + request.add(new Item(SONG_INDEX_NAME, ID_S2)); + + MultiGetResponse response = restHighLevelClient.mget(request, DEFAULT); + + assertThat(response, is(notNullValue())); + assertThat(response, isSuccessfulMultiGetResponse()); + assertThat(response, numberOfGetItemResponsesIsEqualTo(2)); + + MultiGetItemResponse[] responses = response.getResponses(); + assertThat( + responses[0].getResponse(), + allOf(containDocument(SONG_INDEX_NAME, ID_S1), documentContainField(FIELD_TITLE, TITLE_MAGNUM_OPUS)) + ); + assertThat( + responses[1].getResponse(), + allOf(containDocument(SONG_INDEX_NAME, ID_S2), documentContainField(FIELD_TITLE, TITLE_SONG_1_PLUS_1)) + ); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_mget")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiGetRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiGetShardRequest")); + } + + @Test + public void shouldPerformMultiGetDocuments_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { + MultiGetRequest request = new MultiGetRequest(); + request.add(new Item(SONG_INDEX_NAME, ID_S1)); + + assertThatThrownBy(() -> restHighLevelClient.mget(request, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/_mget")); + auditLogsRule.assertExactlyOne(missingPrivilege(DOUBLE_READER_USER, "MultiGetRequest")); + } + + @Test + public void shouldPerformMultiGetDocuments_partiallyPositive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + MultiGetRequest request = new MultiGetRequest(); + request.add(new Item(SONG_INDEX_NAME, ID_S1)); + request.add(new Item(PROHIBITED_SONG_INDEX_NAME, ID_P4)); + + MultiGetResponse response = restHighLevelClient.mget(request, DEFAULT); + + assertThat(request, notNullValue()); + assertThat(response, not(isSuccessfulMultiGetResponse())); + assertThat(response, numberOfGetItemResponsesIsEqualTo(2)); + + MultiGetItemResponse[] responses = response.getResponses(); + assertThat(responses, arrayContaining(hasProperty("failure", nullValue()), hasProperty("failure", notNullValue()))); + assertThat(responses[1].getFailure().getFailure(), statusException(INTERNAL_SERVER_ERROR)); + assertThat(responses[1].getFailure().getFailure(), errorMessageContain("security_exception")); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_mget")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiGetRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiGetShardRequest").withIndex(SONG_INDEX_NAME)); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "MultiGetShardRequest").withIndex(PROHIBITED_SONG_INDEX_NAME)); + } + + @Test + public void shouldBeAllowedToPerformMulitSearch_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + MultiSearchRequest request = new MultiSearchRequest(); + request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS)); + request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_NEXT_SONG)); + + MultiSearchResponse response = restHighLevelClient.msearch(request, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response, isSuccessfulMultiSearchResponse()); + assertThat(response, numberOfSearchItemResponsesIsEqualTo(2)); + + MultiSearchResponse.Item[] responses = response.getResponses(); + + assertThat(responses[0].getResponse(), searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + assertThat(responses[0].getResponse(), searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S1)); + assertThat(responses[1].getResponse(), searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_NEXT_SONG)); + assertThat(responses[1].getResponse(), searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, ID_S3)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_msearch")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiSearchRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldBeAllowedToPerformMulitSearch_partiallyPositive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + MultiSearchRequest request = new MultiSearchRequest(); + request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS)); + request.add(queryStringQueryRequest(PROHIBITED_SONG_INDEX_NAME, QUERY_TITLE_POISON)); + + MultiSearchResponse response = restHighLevelClient.msearch(request, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response, not(isSuccessfulMultiSearchResponse())); + assertThat(response, numberOfSearchItemResponsesIsEqualTo(2)); + + MultiSearchResponse.Item[] responses = response.getResponses(); + assertThat(responses[0].getFailure(), nullValue()); + assertThat(responses[1].getFailure(), statusException(INTERNAL_SERVER_ERROR)); + assertThat(responses[1].getFailure(), errorMessageContain("security_exception")); + assertThat(responses[1].getResponse(), nullValue()); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_msearch")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "MultiSearchRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest").withIndex(SONG_INDEX_NAME)); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest").withIndex(PROHIBITED_SONG_INDEX_NAME)); + } + + @Test + public void shouldBeAllowedToPerformMulitSearch_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(DOUBLE_READER_USER)) { + MultiSearchRequest request = new MultiSearchRequest(); + request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS)); + request.add(queryStringQueryRequest(SONG_INDEX_NAME, QUERY_TITLE_NEXT_SONG)); + + assertThatThrownBy(() -> restHighLevelClient.msearch(request, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(DOUBLE_READER_USER).withRestRequest(POST, "/_msearch")); + auditLogsRule.assertExactlyOne(missingPrivilege(DOUBLE_READER_USER, "MultiSearchRequest")); + } + + @Test + public void shouldAggregateDataAndComputeAverage_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + final String aggregationName = "averageStars"; + SearchRequest searchRequest = averageAggregationRequest(SONG_INDEX_NAME, aggregationName, FIELD_STARS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "avg")); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest").withIndex(SONG_INDEX_NAME)); + } + + @Test + public void shouldAggregateDataAndComputeAverage_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = averageAggregationRequest(PROHIBITED_SONG_INDEX_NAME, "averageStars", FIELD_STARS); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest").withIndex(PROHIBITED_SONG_INDEX_NAME)); + } + + @Test + public void shouldPerformStatAggregation_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + final String aggregationName = "statsStars"; + SearchRequest searchRequest = statsAggregationRequest(SONG_INDEX_NAME, aggregationName, FIELD_STARS); + + SearchResponse searchResponse = restHighLevelClient.search(searchRequest, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, containAggregationWithNameAndType(aggregationName, "stats")); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/song_lyrics/_search")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldPerformStatAggregation_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SearchRequest searchRequest = statsAggregationRequest(PROHIBITED_SONG_INDEX_NAME, "statsStars", FIELD_STARS); + + assertThatThrownBy(() -> restHighLevelClient.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/prohibited_song_lyrics/_search")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "SearchRequest")); + } + + @Test + public void shouldIndexDocumentInBulkRequest_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); + bulkRequest.setRefreshPolicy(IMMEDIATE); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat(response, successBulkResponse()); + assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, "one")); + assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, "two")); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "one", FIELD_TITLE, TITLE_MAGNUM_OPUS)); + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, TITLE_SONG_1_PLUS_1) + ); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertAtLeast(4, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER));// sometimes 4 or 6 + auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest"));// sometimes 2 or 4 + } + + @Test + public void shouldIndexDocumentInBulkRequest_partiallyPositive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); + bulkRequest.setRefreshPolicy(IMMEDIATE); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat( + response, + bulkResponseContainExceptions(0, allOf(statusException(INTERNAL_SERVER_ERROR), errorMessageContain("security_exception"))) + ); + assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, "two")); + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, TITLE_SONG_1_PLUS_1) + ); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest").withIndex(SONG_INDEX_NAME)); + } + + @Test + public void shouldIndexDocumentInBulkRequest_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); + bulkRequest.setRefreshPolicy(IMMEDIATE); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat( + response, + allOf( + failureBulkResponse(), + bulkResponseContainExceptions(statusException(INTERNAL_SERVER_ERROR)), + bulkResponseContainExceptions(errorMessageContain("security_exception")) + ) + ); + assertThat(internalClient, not(clusterContainsDocument(SONG_INDEX_NAME, "one"))); + assertThat(internalClient, not(clusterContainsDocument(SONG_INDEX_NAME, "two"))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest").withIndex(SONG_INDEX_NAME)); + } + + @Test + public void shouldUpdateDocumentsInBulk_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + final String titleOne = "shape of my mind"; + final String titleTwo = "forgiven"; + BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); + restHighLevelClient.bulk(bulkRequest, DEFAULT); + bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new UpdateRequest(WRITE_SONG_INDEX_NAME, "one").doc(Map.of(FIELD_TITLE, titleOne))); + bulkRequest.add(new UpdateRequest(WRITE_SONG_INDEX_NAME, "two").doc(Map.of(FIELD_TITLE, titleTwo))); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat(response, successBulkResponse()); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "one", FIELD_TITLE, titleOne)); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, titleTwo)); + } + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + + } + + @Test + public void shouldUpdateDocumentsInBulk_partiallyPositive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + final String titleOne = "shape of my mind"; + BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + restHighLevelClient.bulk(bulkRequest, DEFAULT); + bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new UpdateRequest(WRITE_SONG_INDEX_NAME, "one").doc(Map.of(FIELD_TITLE, titleOne))); + bulkRequest.add(new UpdateRequest(SONG_INDEX_NAME, ID_S2).doc(Map.of(FIELD_TITLE, "forgiven"))); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat( + response, + bulkResponseContainExceptions(1, allOf(statusException(INTERNAL_SERVER_ERROR), errorMessageContain("security_exception"))) + ); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "one", FIELD_TITLE, titleOne)); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(SONG_INDEX_NAME, ID_S2, FIELD_TITLE, TITLE_SONG_1_PLUS_1)); + } + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest").withIndex(SONG_INDEX_NAME)); + } + + @Test + public void shouldUpdateDocumentsInBulk_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new UpdateRequest(SONG_INDEX_NAME, ID_S1).doc(Map.of(FIELD_TITLE, "shape of my mind"))); + bulkRequest.add(new UpdateRequest(SONG_INDEX_NAME, ID_S2).doc(Map.of(FIELD_TITLE, "forgiven"))); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat( + response, + allOf( + failureBulkResponse(), + bulkResponseContainExceptions(statusException(INTERNAL_SERVER_ERROR)), + bulkResponseContainExceptions(errorMessageContain("security_exception")) + ) + ); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(SONG_INDEX_NAME, ID_S1, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(SONG_INDEX_NAME, ID_S2, FIELD_TITLE, TITLE_SONG_1_PLUS_1)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest")); + } + + @Test + public void shouldDeleteDocumentInBulk_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("three").source(SONGS[2].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("four").source(SONGS[3].asMap())); + assertThat(restHighLevelClient.bulk(bulkRequest, DEFAULT), successBulkResponse()); + bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new DeleteRequest(WRITE_SONG_INDEX_NAME, "one")); + bulkRequest.add(new DeleteRequest(WRITE_SONG_INDEX_NAME, "three")); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat(response, successBulkResponse()); + assertThat(internalClient, not(clusterContainsDocument(WRITE_SONG_INDEX_NAME, "one"))); + assertThat(internalClient, not(clusterContainsDocument(WRITE_SONG_INDEX_NAME, "three"))); + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, TITLE_SONG_1_PLUS_1) + ); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "four", FIELD_TITLE, TITLE_POISON)); + } + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + } + + @Test + public void shouldDeleteDocumentInBulk_partiallyPositive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("one").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("two").source(SONGS[1].asMap())); + assertThat(restHighLevelClient.bulk(bulkRequest, DEFAULT), successBulkResponse()); + bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new DeleteRequest(WRITE_SONG_INDEX_NAME, "one")); + bulkRequest.add(new DeleteRequest(SONG_INDEX_NAME, ID_S3)); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + assertThat(internalClient, not(clusterContainsDocument(WRITE_SONG_INDEX_NAME, "one"))); + + assertThat( + response, + bulkResponseContainExceptions(1, allOf(statusException(INTERNAL_SERVER_ERROR), errorMessageContain("security_exception"))) + ); + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(WRITE_SONG_INDEX_NAME, "two", FIELD_TITLE, TITLE_SONG_1_PLUS_1) + ); + assertThat(internalClient, clusterContainsDocumentWithFieldValue(SONG_INDEX_NAME, ID_S3, FIELD_TITLE, TITLE_NEXT_SONG)); + } + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest")); + } + + @Test + public void shouldDeleteDocumentInBulk_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + BulkRequest bulkRequest = new BulkRequest().setRefreshPolicy(IMMEDIATE); + bulkRequest.add(new DeleteRequest(SONG_INDEX_NAME, ID_S1)); + bulkRequest.add(new DeleteRequest(SONG_INDEX_NAME, ID_S3)); + + BulkResponse response = restHighLevelClient.bulk(bulkRequest, DEFAULT); + + assertThat( + response, + allOf( + failureBulkResponse(), + bulkResponseContainExceptions(statusException(INTERNAL_SERVER_ERROR)), + bulkResponseContainExceptions(errorMessageContain("security_exception")) + ) + ); + assertThat(internalClient, clusterContainsDocument(SONG_INDEX_NAME, ID_S1)); + assertThat(internalClient, clusterContainsDocument(SONG_INDEX_NAME, ID_S3)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "BulkShardRequest")); + + } + + @Test + public void shouldReindexDocuments_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { + ReindexRequest reindexRequest = new ReindexRequest().setSourceIndices(SONG_INDEX_NAME).setDestIndex(WRITE_SONG_INDEX_NAME); + + BulkByScrollResponse response = restHighLevelClient.reindex(reindexRequest, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response.getBulkFailures(), empty()); + assertThat(response.getSearchFailures(), empty()); + assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, ID_S1)); + assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, ID_S2)); + assertThat(internalClient, clusterContainsDocument(WRITE_SONG_INDEX_NAME, ID_S3)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(REINDEXING_USER).withRestRequest(POST, "/_reindex")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "ReindexRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "SearchRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "BulkRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(REINDEXING_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(4, grantedPrivilege(REINDEXING_USER, "PutMappingRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "SearchScrollRequest")); + auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(REINDEXING_USER)); + auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "ClearScrollRequest")); + } + + @Test + public void shouldReindexDocuments_negativeSource() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { + ReindexRequest reindexRequest = new ReindexRequest().setSourceIndices(PROHIBITED_SONG_INDEX_NAME) + .setDestIndex(WRITE_SONG_INDEX_NAME); + + assertThatThrownBy(() -> restHighLevelClient.reindex(reindexRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(internalClient, not(clusterContainsDocument(WRITE_SONG_INDEX_NAME, ID_P4))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(REINDEXING_USER).withRestRequest(POST, "/_reindex")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "ReindexRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "SearchRequest")); + } + + @Test + public void shouldReindexDocuments_negativeDestination() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { + ReindexRequest reindexRequest = new ReindexRequest().setSourceIndices(SONG_INDEX_NAME).setDestIndex(PROHIBITED_SONG_INDEX_NAME); + + assertThatThrownBy(() -> restHighLevelClient.reindex(reindexRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(internalClient, not(clusterContainsDocument(PROHIBITED_SONG_INDEX_NAME, ID_S1))); + assertThat(internalClient, not(clusterContainsDocument(PROHIBITED_SONG_INDEX_NAME, ID_S2))); + assertThat(internalClient, not(clusterContainsDocument(PROHIBITED_SONG_INDEX_NAME, ID_S3))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(REINDEXING_USER).withRestRequest(POST, "/_reindex")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "ReindexRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "SearchRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "BulkRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "BulkShardRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "ClearScrollRequest")); + } + + @Test + public void shouldReindexDocuments_negativeSourceAndDestination() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(REINDEXING_USER)) { + ReindexRequest reindexRequest = new ReindexRequest().setSourceIndices(PROHIBITED_SONG_INDEX_NAME).setDestIndex(SONG_INDEX_NAME); + + assertThatThrownBy(() -> restHighLevelClient.reindex(reindexRequest, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(REINDEXING_USER).withRestRequest(POST, "/_reindex")); + auditLogsRule.assertExactlyOne(grantedPrivilege(REINDEXING_USER, "ReindexRequest")); + auditLogsRule.assertExactlyOne(missingPrivilege(REINDEXING_USER, "SearchRequest")); + } + + @Test + public void shouldUpdateDocument_positive() throws IOException { + String newField = "newField"; + String newValue = "newValue"; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(UPDATE_DELETE_USER)) { + UpdateRequest updateRequest = new UpdateRequest(UPDATE_DELETE_OPERATION_INDEX_NAME, DOCUMENT_TO_UPDATE_ID).doc( + newField, + newValue + ).setRefreshPolicy(IMMEDIATE); + + UpdateResponse response = restHighLevelClient.update(updateRequest, DEFAULT); + + assertThat(response, isSuccessfulUpdateResponse()); + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(UPDATE_DELETE_OPERATION_INDEX_NAME, DOCUMENT_TO_UPDATE_ID, newField, newValue) + ); + } + } + + @Test + public void shouldUpdateDocument_negative() throws IOException { + String newField = "newField"; + String newValue = "newValue"; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(UPDATE_DELETE_USER)) { + UpdateRequest updateRequest = new UpdateRequest(PROHIBITED_SONG_INDEX_NAME, DOCUMENT_TO_UPDATE_ID).doc(newField, newValue) + .setRefreshPolicy(IMMEDIATE); + + assertThatThrownBy(() -> restHighLevelClient.update(updateRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldDeleteDocument_positive() throws IOException { + String docId = "shouldDeleteDocument_positive"; + try (Client client = cluster.getInternalNodeClient()) { + client.index( + new IndexRequest(UPDATE_DELETE_OPERATION_INDEX_NAME).id(docId).source("field", "value").setRefreshPolicy(IMMEDIATE) + ).actionGet(); + assertThat(internalClient, clusterContainsDocument(UPDATE_DELETE_OPERATION_INDEX_NAME, docId)); + } + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(UPDATE_DELETE_USER)) { + DeleteRequest deleteRequest = new DeleteRequest(UPDATE_DELETE_OPERATION_INDEX_NAME, docId).setRefreshPolicy(IMMEDIATE); + + DeleteResponse response = restHighLevelClient.delete(deleteRequest, DEFAULT); + + assertThat(response, isSuccessfulDeleteResponse()); + assertThat(internalClient, not(clusterContainsDocument(UPDATE_DELETE_OPERATION_INDEX_NAME, docId))); + } + } + + @Test + public void shouldDeleteDocument_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(UPDATE_DELETE_USER)) { + DeleteRequest deleteRequest = new DeleteRequest(PROHIBITED_SONG_INDEX_NAME, ID_S1).setRefreshPolicy(IMMEDIATE); + + assertThatThrownBy(() -> restHighLevelClient.delete(deleteRequest, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldCreateAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + AliasActions aliasAction = new AliasActions(ADD).indices(SONG_INDEX_NAME).alias(TEMPORARY_ALIAS_NAME); + IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); + + var response = restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, clusterContainsDocument(TEMPORARY_ALIAS_NAME, ID_S1)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); + auditLogsRule.assertExactly(2, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_READ_USER)); + } + + @Test + public void shouldCreateAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + AliasActions aliasAction = new AliasActions(ADD).indices(PROHIBITED_SONG_INDEX_NAME).alias(TEMPORARY_ALIAS_NAME); + IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); + + assertThatThrownBy( + () -> restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT), + statusException(FORBIDDEN) + ); + + assertThat(internalClient, not(clusterContainsDocument(TEMPORARY_ALIAS_NAME, ID_P4))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); + } + + @Test + public void shouldDeleteAlias_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + AliasActions aliasAction = new AliasActions(ADD).indices(SONG_INDEX_NAME).alias(TEMPORARY_ALIAS_NAME); + IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); + restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT); + aliasAction = new AliasActions(REMOVE).indices(SONG_INDEX_NAME).alias(TEMPORARY_ALIAS_NAME); + indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); + + var response = restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, not(clusterContainsDocument(TEMPORARY_ALIAS_NAME, ID_S1))); + } + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); + auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); + auditLogsRule.assertExactly(4, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_READ_USER)); + } + + @Test + public void shouldDeleteAlias_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + AliasActions aliasAction = new AliasActions(REMOVE).indices(PROHIBITED_SONG_INDEX_NAME).alias(PROHIBITED_SONG_ALIAS); + IndicesAliasesRequest indicesAliasesRequest = new IndicesAliasesRequest().addAliasAction(aliasAction); + + assertThatThrownBy( + () -> restHighLevelClient.indices().updateAliases(indicesAliasesRequest, DEFAULT), + statusException(FORBIDDEN) + ); + + assertThat(internalClient, clusterContainsDocument(PROHIBITED_SONG_INDEX_NAME, ID_P4)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(POST, "/_aliases")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "IndicesAliasesRequest")); + } + + @Test + public void shouldCreateIndexTemplate_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + PutIndexTemplateRequest request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE).patterns(List.of(TEMPLATE_INDEX_PREFIX)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002)); + + var response = restHighLevelClient.indices().putTemplate(request, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, clusterContainTemplate(MUSICAL_INDEX_TEMPLATE)); + String documentId = "0001"; + IndexRequest indexRequest = new IndexRequest(INDEX_NAME_SONG_TRANSCRIPTION_JAZZ).id(documentId) + .source(SONGS[0].asMap()) + .setRefreshPolicy(IMMEDIATE); + restHighLevelClient.index(indexRequest, DEFAULT); + assertThat(internalClient, clusterContainsDocument(INDEX_NAME_SONG_TRANSCRIPTION_JAZZ, documentId)); + assertThat(internalClient, clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001, documentId)); + assertThat(internalClient, clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002, documentId)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_template/musical-index-template")); + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/song-transcription-jazz/_doc/0001")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "IndexRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertExactly(8, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + } + + @Test + public void shouldCreateIndexTemplate_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + PutIndexTemplateRequest request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE).patterns(List.of(TEMPLATE_INDEX_PREFIX)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002)); + + assertThatThrownBy(() -> restHighLevelClient.indices().putTemplate(request, DEFAULT), statusException(FORBIDDEN)); + assertThat(internalClient, not(clusterContainTemplate(MUSICAL_INDEX_TEMPLATE))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(PUT, "/_template/musical-index-template")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "PutIndexTemplateRequest")); + } + + @Test + public void shouldDeleteTemplate_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + PutIndexTemplateRequest request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE).patterns(List.of(TEMPLATE_INDEX_PREFIX)); + restHighLevelClient.indices().putTemplate(request, DEFAULT); + assertThat(internalClient, clusterContainTemplate(MUSICAL_INDEX_TEMPLATE)); + DeleteIndexTemplateRequest deleteRequest = new DeleteIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE); + + var response = restHighLevelClient.indices().deleteTemplate(deleteRequest, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, not(clusterContainTemplate(MUSICAL_INDEX_TEMPLATE))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_template/musical-index-template")); + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(DELETE, "/_template/musical-index-template")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "DeleteIndexTemplateRequest")); + auditLogsRule.assertExactly(4, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + } + + @Test + public void shouldDeleteTemplate_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + DeleteIndexTemplateRequest deleteRequest = new DeleteIndexTemplateRequest(UNDELETABLE_TEMPLATE_NAME); + + assertThatThrownBy(() -> restHighLevelClient.indices().deleteTemplate(deleteRequest, DEFAULT), statusException(FORBIDDEN)); + + assertThat(internalClient, clusterContainTemplate(UNDELETABLE_TEMPLATE_NAME)); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_READ_USER).withRestRequest(DELETE, "/_template/undeletable-template-name") + ); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "DeleteIndexTemplateRequest")); + } + + @Test + public void shouldUpdateTemplate_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + PutIndexTemplateRequest request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE).patterns(List.of(TEMPLATE_INDEX_PREFIX)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002)); + restHighLevelClient.indices().putTemplate(request, DEFAULT); + assertThat(internalClient, clusterContainTemplate(MUSICAL_INDEX_TEMPLATE)); + request = new PutIndexTemplateRequest(MUSICAL_INDEX_TEMPLATE).patterns(List.of(TEMPLATE_INDEX_PREFIX)) + .alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003)); + + var response = restHighLevelClient.indices().putTemplate(request, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + String documentId = "000one"; + IndexRequest indexRequest = new IndexRequest(INDEX_NAME_SONG_TRANSCRIPTION_JAZZ).id(documentId) + .source(SONGS[0].asMap()) + .setRefreshPolicy(IMMEDIATE); + restHighLevelClient.index(indexRequest, DEFAULT); + assertThat(internalClient, clusterContainTemplate(MUSICAL_INDEX_TEMPLATE)); + assertThat(internalClient, clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003, documentId)); + assertThat(internalClient, not(clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0001, documentId))); + assertThat(internalClient, not(clusterContainsDocument(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0002, documentId))); + } + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_template/musical-index-template")); + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/song-transcription-jazz/_doc/000one")); + auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutIndexTemplateRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "IndexRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertExactly(10, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + } + + @Test + public void shouldUpdateTemplate_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + PutIndexTemplateRequest request = new PutIndexTemplateRequest(UNDELETABLE_TEMPLATE_NAME).patterns( + List.of(TEMPLATE_INDEX_PREFIX) + ).alias(new Alias(ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003)); + + assertThatThrownBy(() -> restHighLevelClient.indices().putTemplate(request, DEFAULT), statusException(FORBIDDEN)); + assertThat(internalClient, clusterContainTemplateWithAlias(UNDELETABLE_TEMPLATE_NAME, ALIAS_FROM_UNDELETABLE_TEMPLATE)); + assertThat( + internalClient, + not(clusterContainTemplateWithAlias(UNDELETABLE_TEMPLATE_NAME, ALIAS_USED_IN_MUSICAL_INDEX_TEMPLATE_0003)) + ); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(PUT, "/_template/undeletable-template-name")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "PutIndexTemplateRequest")); + } + + @Test + public void shouldGetFieldCapabilitiesForAllIndexes_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(ADMIN_USER)) { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().fields(FIELD_TITLE); + + FieldCapabilitiesResponse response = restHighLevelClient.fieldCaps(request, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response, containsExactlyIndices(SONG_INDEX_NAME, PROHIBITED_SONG_INDEX_NAME, UPDATE_DELETE_OPERATION_INDEX_NAME)); + assertThat(response, numberOfFieldsIsEqualTo(1)); + assertThat(response, containsFieldWithNameAndType(FIELD_TITLE, "text")); + } + auditLogsRule.assertExactlyOne(userAuthenticated(ADMIN_USER).withRestRequest(GET, "/_field_caps")); + auditLogsRule.assertExactlyOne(grantedPrivilege(ADMIN_USER, "FieldCapabilitiesRequest")); + auditLogsRule.assertExactly(3, grantedPrivilege(ADMIN_USER, "FieldCapabilitiesIndexRequest")); + } + + @Test + public void shouldGetFieldCapabilitiesForAllIndexes_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().fields(FIELD_TITLE); + + assertThatThrownBy(() -> restHighLevelClient.fieldCaps(request, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/_field_caps")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "FieldCapabilitiesRequest")); + } + + @Test + public void shouldGetFieldCapabilitiesForParticularIndex_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(SONG_INDEX_NAME).fields(FIELD_TITLE); + + FieldCapabilitiesResponse response = restHighLevelClient.fieldCaps(request, DEFAULT); + + assertThat(response, notNullValue()); + assertThat(response, containsExactlyIndices(SONG_INDEX_NAME)); + assertThat(response, numberOfFieldsIsEqualTo(1)); + assertThat(response, containsFieldWithNameAndType(FIELD_TITLE, "text")); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/song_lyrics/_field_caps")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "FieldCapabilitiesRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_READ_USER, "FieldCapabilitiesIndexRequest")); + } + + @Test + public void shouldGetFieldCapabilitiesForParticularIndex_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + FieldCapabilitiesRequest request = new FieldCapabilitiesRequest().indices(PROHIBITED_SONG_INDEX_NAME).fields(FIELD_TITLE); + + assertThatThrownBy(() -> restHighLevelClient.fieldCaps(request, DEFAULT), statusException(FORBIDDEN)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(GET, "/prohibited_song_lyrics/_field_caps")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "FieldCapabilitiesRequest")); + } + + @Test + public void shouldCreateSnapshotRepository_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + String snapshotDirPath = cluster.getSnapshotDirPath(); + + var response = steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotDirPath, "fs"); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, clusterContainsSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + } + + @Test + public void shouldCreateSnapshotRepository_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + String snapshotDirPath = cluster.getSnapshotDirPath(); + + assertThatThrownBy( + () -> steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotDirPath, "fs"), + statusException(FORBIDDEN) + ); + assertThat(internalClient, not(clusterContainsSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_READ_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "PutRepositoryRequest")); + } + + @Test + public void shouldDeleteSnapshotRepository_positive() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + assertThat(internalClient, clusterContainsSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME)); + + var response = steps.deleteSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME); + + assertThat(response, notNullValue()); + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, not(clusterContainsSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(DELETE, "/_snapshot/test-snapshot-repository") + ); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "DeleteRepositoryRequest")); + } + + @Test + public void shouldDeleteSnapshotRepository_negative() throws IOException { + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + + assertThatThrownBy(() -> steps.deleteSnapshotRepository(UNUSED_SNAPSHOT_REPOSITORY_NAME), statusException(FORBIDDEN)); + assertThat(internalClient, clusterContainsSnapshotRepository(UNUSED_SNAPSHOT_REPOSITORY_NAME)); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_READ_USER).withRestRequest(DELETE, "/_snapshot/unused-snapshot-repository") + ); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "DeleteRepositoryRequest")); + } + + @Test // Bug which can be reproduced with the below test: https://github.com/opensearch-project/security/issues/2169 + public void shouldCreateSnapshot_positive() throws IOException { + final String snapshotName = "snapshot-positive-test"; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + + CreateSnapshotResponse response = steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME); + + assertThat(response, notNullValue()); + assertThat(response.status(), equalTo(RestStatus.ACCEPTED)); + steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + assertThat(internalClient, clusterContainSuccessSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/snapshot-positive-test") + ); + auditLogsRule.assertAtLeast( + 1, + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/snapshot-positive-test") + ); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + } + + @Test + public void shouldCreateSnapshot_negative() throws IOException { + final String snapshotName = "snapshot-negative-test"; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + + assertThatThrownBy( + () -> steps.createSnapshot(UNUSED_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME), + statusException(FORBIDDEN) + ); + + assertThat(internalClient, snapshotInClusterDoesNotExists(UNUSED_SNAPSHOT_REPOSITORY_NAME, snapshotName)); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_READ_USER).withRestRequest(PUT, "/_snapshot/unused-snapshot-repository/snapshot-negative-test") + ); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_READ_USER, "CreateSnapshotRequest")); + } + + @Test + public void shouldDeleteSnapshot_positive() throws IOException { + String snapshotName = "delete-snapshot-positive"; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + restHighLevelClient.snapshot(); + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME); + steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + + var response = steps.deleteSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + + assertThat(response.isAcknowledged(), equalTo(true)); + assertThat(internalClient, snapshotInClusterDoesNotExists(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/delete-snapshot-positive") + ); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(DELETE, "/_snapshot/test-snapshot-repository/delete-snapshot-positive") + ); + auditLogsRule.assertAtLeast( + 1, + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/delete-snapshot-positive") + ); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "DeleteSnapshotRequest")); + auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + } + + @Test + public void shouldDeleteSnapshot_negative() throws IOException { + String snapshotName = "delete-snapshot-negative"; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, SONG_INDEX_NAME); + steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + } + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + assertThatThrownBy(() -> steps.deleteSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName), statusException(FORBIDDEN)); + + assertThat(internalClient, clusterContainSuccessSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName)); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/delete-snapshot-negative") + ); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_READ_USER).withRestRequest(DELETE, "/_snapshot/test-snapshot-repository/delete-snapshot-negative") + ); + auditLogsRule.assertAtLeast( + 1, + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/delete-snapshot-negative") + ); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactly(1, missingPrivilege(LIMITED_READ_USER, "DeleteSnapshotRequest")); + auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + } + + @Test + public void shouldRestoreSnapshot_positive() throws IOException { + final String snapshotName = "restore-snapshot-positive"; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + // 1. create some documents + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Eins").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Zwei").source(SONGS[1].asMap())); + bulkRequest.setRefreshPolicy(IMMEDIATE); + restHighLevelClient.bulk(bulkRequest, DEFAULT); + + // 2. create snapshot repository + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + + // 3. create snapshot + steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, WRITE_SONG_INDEX_NAME); + + // 4. wait till snapshot is ready + steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + + // 5. introduce some changes + bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Drei").source(SONGS[2].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Vier").source(SONGS[3].asMap())); + bulkRequest.add(new DeleteRequest(WRITE_SONG_INDEX_NAME, "Eins")); + bulkRequest.setRefreshPolicy(IMMEDIATE); + restHighLevelClient.bulk(bulkRequest, DEFAULT); + + // 6. restore the snapshot + var response = steps.restoreSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, "(.+)", "restored_$1"); + + assertThat(response, notNullValue()); + assertThat(response.status(), equalTo(ACCEPTED)); + + // 7. wait until snapshot is restored + CountRequest countRequest = new CountRequest(RESTORED_SONG_INDEX_NAME); + Awaitility.await() + .ignoreExceptions() + .alias("Index contains proper number of documents restored from snapshot.") + .until(() -> restHighLevelClient.count(countRequest, DEFAULT).getCount() == 2); + + // 8. verify that document are present in restored index + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(RESTORED_SONG_INDEX_NAME, "Eins", FIELD_TITLE, TITLE_MAGNUM_OPUS) + ); + assertThat( + internalClient, + clusterContainsDocumentWithFieldValue(RESTORED_SONG_INDEX_NAME, "Zwei", FIELD_TITLE, TITLE_SONG_1_PLUS_1) + ); + assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Drei"))); + assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Vier"))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository/restore-snapshot-positive") + ); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest( + POST, + "/_snapshot/test-snapshot-repository/restore-snapshot-positive/_restore" + ) + ); + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/restored_write_song_index/_count")); + auditLogsRule.assertExactly(2, userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertAtLeast( + 1, + userAuthenticated(LIMITED_WRITE_USER).withRestRequest(GET, "/_snapshot/test-snapshot-repository/restore-snapshot-positive") + ); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "RestoreSnapshotRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "SearchRequest")); + auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + } + + @Test + public void shouldRestoreSnapshot_failureForbiddenIndex() throws IOException { + final String snapshotName = "restore-snapshot-negative-forbidden-index"; + String restoreToIndex = "forbidden_index"; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + // 1. create some documents + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Eins").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Zwei").source(SONGS[1].asMap())); + bulkRequest.setRefreshPolicy(IMMEDIATE); + restHighLevelClient.bulk(bulkRequest, DEFAULT); + + // 2. create snapshot repository + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + + // 3. create snapshot + steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, WRITE_SONG_INDEX_NAME); + + // 4. wait till snapshot is ready + steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + + // 5. restore the snapshot + assertThatThrownBy( + () -> steps.restoreSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, "(.+)", restoreToIndex), + statusException(FORBIDDEN) + ); + + // 6. verify that document are not present in restored index + assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Eins"))); + assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Zwei"))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest( + PUT, + "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-index" + ) + ); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest( + POST, + "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-index/_restore" + ) + ); + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertAtLeast( + 1, + userAuthenticated(LIMITED_WRITE_USER).withRestRequest( + GET, + "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-index" + ) + ); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "RestoreSnapshotRequest")); + auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + auditLogsRule.assertExactlyOne(missingPrivilege(LIMITED_WRITE_USER, "RestoreSnapshotRequest")); + } + + @Test + public void shouldRestoreSnapshot_failureOperationForbidden() throws IOException { + String snapshotName = "restore-snapshot-negative-forbidden-operation"; + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_WRITE_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + // 1. create some documents + BulkRequest bulkRequest = new BulkRequest(); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Eins").source(SONGS[0].asMap())); + bulkRequest.add(new IndexRequest(WRITE_SONG_INDEX_NAME).id("Zwei").source(SONGS[1].asMap())); + bulkRequest.setRefreshPolicy(IMMEDIATE); + restHighLevelClient.bulk(bulkRequest, DEFAULT); + + // 2. create snapshot repository + steps.createSnapshotRepository(TEST_SNAPSHOT_REPOSITORY_NAME, cluster.getSnapshotDirPath(), "fs"); + + // 3. create snapshot + steps.createSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, WRITE_SONG_INDEX_NAME); + + // 4. wait till snapshot is ready + steps.waitForSnapshotCreation(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName); + } + // 5. restore the snapshot + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(LIMITED_READ_USER)) { + SnapshotSteps steps = new SnapshotSteps(restHighLevelClient); + assertThatThrownBy( + () -> steps.restoreSnapshot(TEST_SNAPSHOT_REPOSITORY_NAME, snapshotName, "(.+)", "restored_$1"), + statusException(FORBIDDEN) + ); + + // 6. verify that documents does not exist + assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Eins"))); + assertThat(internalClient, not(clusterContainsDocument(RESTORED_SONG_INDEX_NAME, "Zwei"))); + } + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(PUT, "/_snapshot/test-snapshot-repository")); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_WRITE_USER).withRestRequest( + PUT, + "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-operation" + ) + ); + auditLogsRule.assertExactlyOne( + userAuthenticated(LIMITED_READ_USER).withRestRequest( + POST, + "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-operation/_restore" + ) + ); + auditLogsRule.assertExactlyOne(userAuthenticated(LIMITED_WRITE_USER).withRestRequest(POST, "/_bulk")); + auditLogsRule.assertAtLeast( + 1, + userAuthenticated(LIMITED_WRITE_USER).withRestRequest( + GET, + "/_snapshot/test-snapshot-repository/restore-snapshot-negative-forbidden-operation" + ) + ); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "PutRepositoryRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateSnapshotRequest")); + auditLogsRule.assertExactlyOne(grantedPrivilege(LIMITED_WRITE_USER, "BulkRequest")); + auditLogsRule.assertExactly(2, grantedPrivilege(LIMITED_WRITE_USER, "CreateIndexRequest")); + auditLogsRule.assertExactly(4, grantedPrivilege(LIMITED_WRITE_USER, "PutMappingRequest")); + auditLogsRule.assertExactly(1, missingPrivilege(LIMITED_READ_USER, "RestoreSnapshotRequest")); + auditLogsRule.assertAtLeast(2, grantedPrivilege(LIMITED_WRITE_USER, "GetSnapshotsRequest")); + auditLogsRule.assertExactly(6, auditPredicate(INDEX_EVENT).withEffectiveUser(LIMITED_WRITE_USER)); + } + + @Test + // required permissions: "indices:admin/create" + public void createIndex_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_positive"); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName); + CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest, DEFAULT); + + assertThat(createIndexResponse, isSuccessfulCreateIndexResponse(indexName)); + assertThat(cluster, indexExists(indexName)); + } + } + + @Test + public void createIndex_negative() throws IOException { + String indexName = "create_index_negative"; + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName); + + assertThatThrownBy(() -> restHighLevelClient.indices().create(createIndexRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(indexName))); + } + } + + @Test + // required permissions: "indices:admin/get" + public void checkIfIndexExists_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("index_exists_positive"); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + boolean exists = restHighLevelClient.indices().exists(new GetIndexRequest(indexName), DEFAULT); + + assertThat(exists, is(false)); + } + } + + @Test + public void checkIfIndexExists_negative() throws IOException { + String indexThatUserHasNoAccessTo = "index_exists_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + assertThatThrownBy( + () -> restHighLevelClient.indices().exists(new GetIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .exists(new GetIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy(() -> restHighLevelClient.indices().exists(new GetIndexRequest("*"), DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + // required permissions: "indices:admin/delete" + public void deleteIndex_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("delete_index_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest(indexName); + var response = restHighLevelClient.indices().delete(deleteIndexRequest, DEFAULT); + + assertThat(response.isAcknowledged(), is(true)); + assertThat(cluster, not(indexExists(indexName))); + } + } + + @Test + public void deleteIndex_negative() throws IOException { + String indexThatUserHasNoAccessTo = "delete_index_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices().delete(new DeleteIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .delete(new DeleteIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().delete(new DeleteIndexRequest("*"), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + // required permissions: indices:admin/aliases, indices:admin/delete + public void shouldDeleteIndexByAliasRequest_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("delete_index_by_alias_request_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + IndicesAliasesRequest request = new IndicesAliasesRequest().addAliasAction(new AliasActions(REMOVE_INDEX).indices(indexName)); + + var response = restHighLevelClient.indices().updateAliases(request, DEFAULT); + + assertThat(response.isAcknowledged(), is(true)); + assertThat(cluster, not(indexExists(indexName))); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES).withRestRequest(POST, "/_aliases") + ); + auditLogsRule.assertExactly( + 2, + grantedPrivilege(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, "IndicesAliasesRequest") + ); + auditLogsRule.assertExactly( + 2, + auditPredicate(INDEX_EVENT).withEffectiveUser(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES) + ); + } + + @Test + public void shouldDeleteIndexByAliasRequest_negative() throws IOException { + String indexName = "delete_index_by_alias_request_negative"; + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + IndicesAliasesRequest request = new IndicesAliasesRequest().addAliasAction(new AliasActions(REMOVE_INDEX).indices(indexName)); + + assertThatThrownBy(() -> restHighLevelClient.indices().updateAliases(request, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + // required permissions: "indices:admin/get" + public void getIndex_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("get_index_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + GetIndexRequest getIndexRequest = new GetIndexRequest(indexName); + GetIndexResponse response = restHighLevelClient.indices().get(getIndexRequest, DEFAULT); + + assertThat(response, getIndexResponseContainsIndices(indexName)); + } + } + + @Test + public void getIndex_negative() throws IOException { + String indexThatUserHasNoAccessTo = "get_index_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices().get(new GetIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().get(new GetIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy(() -> restHighLevelClient.indices().get(new GetIndexRequest("*"), DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + // required permissions: "indices:admin/close", "indices:admin/close*" + public void closeIndex_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("close_index_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + CloseIndexRequest closeIndexRequest = new CloseIndexRequest(indexName); + CloseIndexResponse response = restHighLevelClient.indices().close(closeIndexRequest, DEFAULT); + + assertThat(response, isSuccessfulCloseIndexResponse()); + assertThat(cluster, indexStateIsEqualTo(indexName, IndexMetadata.State.CLOSE)); + } + } + + @Test + public void closeIndex_negative() throws IOException { + String indexThatUserHasNoAccessTo = "close_index_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices().close(new CloseIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .close(new CloseIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy(() -> restHighLevelClient.indices().close(new CloseIndexRequest("*"), DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + // required permissions: "indices:admin/open" + public void openIndex_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("open_index_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + IndexOperationsHelper.closeIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + OpenIndexRequest closeIndexRequest = new OpenIndexRequest(indexName); + OpenIndexResponse response = restHighLevelClient.indices().open(closeIndexRequest, DEFAULT); + + assertThat(response, isSuccessfulOpenIndexResponse()); + assertThat(cluster, indexStateIsEqualTo(indexName, IndexMetadata.State.OPEN)); + } + } + + @Test + public void openIndex_negative() throws IOException { + String indexThatUserHasNoAccessTo = "open_index_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices().open(new OpenIndexRequest(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .open(new OpenIndexRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy(() -> restHighLevelClient.indices().open(new OpenIndexRequest("*"), DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + @Ignore + // required permissions: "indices:admin/resize", "indices:monitor/stats + // todo even when I assign the `indices:admin/resize` and `indices:monitor/stats` permissions to test user, this test fails. + // Issue: https://github.com/opensearch-project/security/issues/2141 + public void shrinkIndex_positive() throws IOException { + String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_positive_source"); + Settings sourceIndexSettings = Settings.builder().put("index.blocks.write", true).put("index.number_of_shards", 2).build(); + String targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_positive_target"); + IndexOperationsHelper.createIndex(cluster, sourceIndexName, sourceIndexSettings); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + ResizeResponse response = restHighLevelClient.indices().shrink(resizeRequest, DEFAULT); + + assertThat(response, isSuccessfulResizeResponse(targetIndexName)); + assertThat(cluster, indexExists(targetIndexName)); + } + } + + @Test + public void shrinkIndex_negative() throws IOException { + // user cannot access target index + String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_negative_source"); + String targetIndexName = "shrink_index_negative_target"; + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + + assertThatThrownBy(() -> restHighLevelClient.indices().shrink(resizeRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(targetIndexName))); + } + + // user cannot access source index + sourceIndexName = "shrink_index_negative_source"; + targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("shrink_index_negative_target"); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + + assertThatThrownBy(() -> restHighLevelClient.indices().shrink(resizeRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(targetIndexName))); + } + } + + @Test + @Ignore + // required permissions: "indices:admin/resize", "indices:monitor/stats + // todo even when I assign the `indices:admin/resize` and `indices:monitor/stats` permissions to test user, this test fails. + // Issue: https://github.com/opensearch-project/security/issues/2141 + public void cloneIndex_positive() throws IOException { + String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clone_index_positive_source"); + Settings sourceIndexSettings = Settings.builder().put("index.blocks.write", true).build(); + String targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clone_index_positive_target"); + IndexOperationsHelper.createIndex(cluster, sourceIndexName, sourceIndexSettings); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + ResizeResponse response = restHighLevelClient.indices().clone(resizeRequest, DEFAULT); + + assertThat(response, isSuccessfulResizeResponse(targetIndexName)); + assertThat(cluster, indexExists(targetIndexName)); + } + } + + @Test + public void cloneIndex_negative() throws IOException { + // user cannot access target index + String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clone_index_negative_source"); + String targetIndexName = "clone_index_negative_target"; + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + + assertThatThrownBy(() -> restHighLevelClient.indices().clone(resizeRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(targetIndexName))); + } + + // user cannot access source index + sourceIndexName = "clone_index_negative_source"; + targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clone_index_negative_target"); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + + assertThatThrownBy(() -> restHighLevelClient.indices().clone(resizeRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(targetIndexName))); + } + } + + @Test + @Ignore + // required permissions: "indices:admin/resize", "indices:monitor/stats + // todo even when I assign the `indices:admin/resize` and `indices:monitor/stats` permissions to test user, this test fails. + // Issue: https://github.com/opensearch-project/security/issues/2141 + public void splitIndex_positive() throws IOException { + String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("split_index_positive_source"); + Settings sourceIndexSettings = Settings.builder().put("index.blocks.write", true).build(); + String targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("split_index_positive_target"); + IndexOperationsHelper.createIndex(cluster, sourceIndexName, sourceIndexSettings); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + resizeRequest.setSettings(Settings.builder().put("index.number_of_shards", 2).build()); + ResizeResponse response = restHighLevelClient.indices().split(resizeRequest, DEFAULT); + + assertThat(response, isSuccessfulResizeResponse(targetIndexName)); + assertThat(cluster, indexExists(targetIndexName)); + } + } + + @Test + public void splitIndex_negative() throws IOException { + // user cannot access target index + String sourceIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("split_index_negative_source"); + String targetIndexName = "split_index_negative_target"; + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + resizeRequest.setSettings(Settings.builder().put("index.number_of_shards", 2).build()); + + assertThatThrownBy(() -> restHighLevelClient.indices().split(resizeRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(targetIndexName))); + } + + // user cannot access source index + sourceIndexName = "split_index_negative_source"; + targetIndexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("split_index_negative_target"); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ResizeRequest resizeRequest = new ResizeRequest(targetIndexName, sourceIndexName); + resizeRequest.setSettings(Settings.builder().put("index.number_of_shards", 2).build()); + + assertThatThrownBy(() -> restHighLevelClient.indices().split(resizeRequest, DEFAULT), statusException(FORBIDDEN)); + assertThat(cluster, not(indexExists(targetIndexName))); + } + } + + @Test + // required permissions: "indices:monitor/settings/get" + public void getIndexSettings_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("get_index_settings_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + GetSettingsRequest getSettingsRequest = new GetSettingsRequest().indices(indexName); + GetSettingsResponse response = restHighLevelClient.indices().getSettings(getSettingsRequest, DEFAULT); + + assertThat(response, getSettingsResponseContainsIndices(indexName)); + } + } + + @Test + public void getIndexSettings_negative() throws IOException { + String indexThatUserHasNoAccessTo = "get_index_settings_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + assertThatThrownBy( + () -> restHighLevelClient.indices().getSettings(new GetSettingsRequest().indices(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .getSettings(new GetSettingsRequest().indices(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().getSettings(new GetSettingsRequest().indices("*"), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + // required permissions: "indices:admin/settings/update" + public void updateIndexSettings_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("update_index_settings_positive"); + Settings initialSettings = Settings.builder().put("index.number_of_replicas", "2").build(); + Settings updatedSettings = Settings.builder().put("index.number_of_replicas", "4").build(); + IndexOperationsHelper.createIndex(cluster, indexName, initialSettings); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + UpdateSettingsRequest updateSettingsRequest = new UpdateSettingsRequest(indexName).settings(updatedSettings); + var response = restHighLevelClient.indices().putSettings(updateSettingsRequest, DEFAULT); + + assertThat(response.isAcknowledged(), is(true)); + assertThat(cluster, indexSettingsContainValues(indexName, updatedSettings)); + } + } + + @Test + public void updateIndexSettings_negative() throws IOException { + String indexThatUserHasNoAccessTo = "update_index_settings_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + Settings settingsToUpdate = Settings.builder().put("index.number_of_replicas", 2).build(); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices() + .putSettings(new UpdateSettingsRequest(indexThatUserHasNoAccessTo).settings(settingsToUpdate), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .putSettings( + new UpdateSettingsRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo).settings(settingsToUpdate), + DEFAULT + ), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().putSettings(new UpdateSettingsRequest("*").settings(settingsToUpdate), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + // required permissions: indices:admin/mapping/put + public void createIndexMappings_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_mappings_positive"); + Map indexMapping = Map.of("properties", Map.of("message", Map.of("type", "text"))); + IndexOperationsHelper.createIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + PutMappingRequest putMappingRequest = new PutMappingRequest(indexName).source(indexMapping); + var response = restHighLevelClient.indices().putMapping(putMappingRequest, DEFAULT); + + assertThat(response.isAcknowledged(), is(true)); + assertThat(cluster, indexMappingIsEqualTo(indexName, indexMapping)); + } + } + + @Test + public void createIndexMappings_negative() throws IOException { + String indexThatUserHasNoAccessTo = "create_index_mappings_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + Map indexMapping = Map.of("properties", Map.of("message", Map.of("type", "text"))); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices() + .putMapping(new PutMappingRequest(indexThatUserHasNoAccessTo).source(indexMapping), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .putMapping(new PutMappingRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo).source(indexMapping), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().putMapping(new PutMappingRequest("*").source(indexMapping), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + // required permissions: indices:admin/mappings/get + public void getIndexMappings_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("get_index_mappings_positive"); + Map indexMapping = Map.of("properties", Map.of("message", Map.of("type", "text"))); + IndexOperationsHelper.createIndex(cluster, indexName); + IndexOperationsHelper.createMapping(cluster, indexName, indexMapping); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + GetMappingsRequest getMappingsRequest = new GetMappingsRequest().indices(indexName); + GetMappingsResponse response = restHighLevelClient.indices().getMapping(getMappingsRequest, DEFAULT); + + assertThat(response, getMappingsResponseContainsIndices(indexName)); + } + } + + @Test + public void getIndexMappings_negative() throws IOException { + String indexThatUserHasNoAccessTo = "get_index_mappings_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices().getMapping(new GetMappingsRequest().indices(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .getMapping(new GetMappingsRequest().indices(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().getMapping(new GetMappingsRequest().indices("*"), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + // required permissions: "indices:admin/cache/clear" + public void clearIndexCache_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("clear_index_cache_positive"); + IndexOperationsHelper.createIndex(cluster, indexName); + + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + ClearIndicesCacheRequest clearIndicesCacheRequest = new ClearIndicesCacheRequest(indexName); + ClearIndicesCacheResponse response = restHighLevelClient.indices().clearCache(clearIndicesCacheRequest, DEFAULT); + + assertThat(response, isSuccessfulClearIndicesCacheResponse()); + } + } + + @Test + public void clearIndexCache_negative() throws IOException { + String indexThatUserHasNoAccessTo = "clear_index_cache_negative"; + String indexThatUserHasAccessTo = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat(indexThatUserHasNoAccessTo); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + + assertThatThrownBy( + () -> restHighLevelClient.indices().clearCache(new ClearIndicesCacheRequest(indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices() + .clearCache(new ClearIndicesCacheRequest(indexThatUserHasAccessTo, indexThatUserHasNoAccessTo), DEFAULT), + statusException(FORBIDDEN) + ); + assertThatThrownBy( + () -> restHighLevelClient.indices().clearCache(new ClearIndicesCacheRequest("*"), DEFAULT), + statusException(FORBIDDEN) + ); + } + } + + @Test + // required permissions: "indices:admin/create", "indices:admin/aliases" + public void shouldCreateIndexWithAlias_positive() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_with_alias_positive"); + try ( + RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient( + USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES + ) + ) { + CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName).alias( + new Alias(ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE) + ); + + CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest, DEFAULT); + + assertThat(createIndexResponse, isSuccessfulCreateIndexResponse(indexName)); + assertThat(cluster, indexExists(indexName)); + assertThat(internalClient, aliasExists(ALIAS_CREATE_INDEX_WITH_ALIAS_POSITIVE)); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES).withRestRequest( + PUT, + "/index_operations_create_index_with_alias_positive" + ) + ); + auditLogsRule.assertExactly( + 2, + grantedPrivilege(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES, "CreateIndexRequest") + ); + auditLogsRule.assertExactly( + 2, + auditPredicate(INDEX_EVENT).withEffectiveUser(USER_ALLOWED_TO_PERFORM_INDEX_OPERATIONS_ON_SELECTED_INDICES) + ); + } + + @Test + public void shouldCreateIndexWithAlias_negative() throws IOException { + String indexName = INDICES_ON_WHICH_USER_CAN_PERFORM_INDEX_OPERATIONS_PREFIX.concat("create_index_with_alias_negative"); + try (RestHighLevelClient restHighLevelClient = cluster.getRestHighLevelClient(USER_ALLOWED_TO_CREATE_INDEX)) { + CreateIndexRequest createIndexRequest = new CreateIndexRequest(indexName).alias( + new Alias(ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE) + ); + + assertThatThrownBy(() -> restHighLevelClient.indices().create(createIndexRequest, DEFAULT), statusException(FORBIDDEN)); + + assertThat(internalClient, not(aliasExists(ALIAS_CREATE_INDEX_WITH_ALIAS_NEGATIVE))); + } + auditLogsRule.assertExactlyOne( + userAuthenticated(USER_ALLOWED_TO_CREATE_INDEX).withRestRequest(PUT, "/index_operations_create_index_with_alias_negative") + ); + auditLogsRule.assertExactlyOne(missingPrivilege(USER_ALLOWED_TO_CREATE_INDEX, "CreateIndexRequest")); + } } diff --git a/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java b/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java index 997f879225..164b2cb714 100644 --- a/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java +++ b/src/integrationTest/java/org/opensearch/security/SecurityAdminLauncher.java @@ -18,37 +18,30 @@ class SecurityAdminLauncher { - private final TestCertificates certificates; - private int port; - - public SecurityAdminLauncher(int port, TestCertificates certificates) { - this.port = port; - this.certificates = requireNonNull(certificates, "Certificates are required to communicate with cluster."); - } - - public int updateRoleMappings(File roleMappingsConfigurationFile) throws Exception { - String[] commandLineArguments = {"-cacert", certificates.getRootCertificate().getAbsolutePath(), - "-cert", certificates.getAdminCertificate().getAbsolutePath(), - "-key", certificates.getAdminKey(null).getAbsolutePath(), - "-nhnv", - "-p", String.valueOf(port), - "-f", roleMappingsConfigurationFile.getAbsolutePath(), - "-t", "rolesmapping" - }; - - return SecurityAdmin.execute(commandLineArguments); - } - - public int updateConfig(File configFile) throws Exception { - String[] commandLineArguments = {"-cacert", certificates.getRootCertificate().getAbsolutePath(), - "-cert", certificates.getAdminCertificate().getAbsolutePath(), - "-key", certificates.getAdminKey(null).getAbsolutePath(), - "-nhnv", - "-p", String.valueOf(port), - "-f", configFile.getAbsolutePath(), - "-t", "config" - }; - - return SecurityAdmin.execute(commandLineArguments); - } + private final TestCertificates certificates; + private int port; + + public SecurityAdminLauncher(int port, TestCertificates certificates) { + this.port = port; + this.certificates = requireNonNull(certificates, "Certificates are required to communicate with cluster."); + } + + public int updateRoleMappings(File roleMappingsConfigurationFile) throws Exception { + String[] commandLineArguments = { + "-cacert", + certificates.getRootCertificate().getAbsolutePath(), + "-cert", + certificates.getAdminCertificate().getAbsolutePath(), + "-key", + certificates.getAdminKey(null).getAbsolutePath(), + "-nhnv", + "-p", + String.valueOf(port), + "-f", + roleMappingsConfigurationFile.getAbsolutePath(), + "-t", + "rolesmapping" }; + + return SecurityAdmin.execute(commandLineArguments); + } } diff --git a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java index ab87dce65e..b35495e23e 100644 --- a/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java +++ b/src/integrationTest/java/org/opensearch/security/SecurityConfigurationTests.java @@ -15,7 +15,6 @@ import java.util.Map; import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope; -import com.fasterxml.jackson.databind.JsonNode; import org.awaitility.Awaitility; import org.junit.BeforeClass; import org.junit.ClassRule; @@ -45,193 +44,189 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class SecurityConfigurationTests { - private static final User USER_ADMIN = new User("admin").roles(ALL_ACCESS); - private static final User LIMITED_USER = new User("limited-user") - .roles(new Role("limited-role").indexPermissions("indices:data/read/search", "indices:data/read/get").on("user-${user.name}")); - public static final String LIMITED_USER_INDEX = "user-" + LIMITED_USER.getName(); - public static final String ADDITIONAL_USER_1 = "additional00001"; - public static final String ADDITIONAL_PASSWORD_1 = "user 1 fair password"; - - public static final String ADDITIONAL_USER_2 = "additional2"; - public static final String ADDITIONAL_PASSWORD_2 = "user 2 fair password"; - public static final String CREATE_USER_BODY = "{\"password\": \"%s\",\"opendistro_security_roles\": []}"; - public static final String INTERNAL_USERS_RESOURCE = "_plugins/_security/api/internalusers/"; - public static final String ID_1 = "one"; - public static final String PROHIBITED_INDEX = "prohibited"; - public static final String ID_2 = "two"; - - @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) - .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN, LIMITED_USER).anonymousAuth(false) - .nodeSettings(Map.of(SECURITY_RESTAPI_ROLES_ENABLED, List.of("user_" + USER_ADMIN.getName() +"__" + ALL_ACCESS.getName()), - SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, true)) - .build(); - - @Rule - public TemporaryFolder configurationDirectory = new TemporaryFolder(); - - @BeforeClass - public static void initData() { - try(Client client = cluster.getInternalNodeClient()){ - client.prepareIndex(LIMITED_USER_INDEX).setId(ID_1).setRefreshPolicy(IMMEDIATE).setSource("foo", "bar").get(); - client.prepareIndex(PROHIBITED_INDEX).setId(ID_2).setRefreshPolicy(IMMEDIATE).setSource("three", "four").get(); - } - } - - @Test - public void shouldCreateUserViaRestApi_success() { - try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - HttpResponse httpResponse = client.putJson(INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_1, String.format(CREATE_USER_BODY, - ADDITIONAL_PASSWORD_1)); - - assertThat(httpResponse.getStatusCode(), equalTo(201)); - } - try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - client.assertCorrectCredentials(USER_ADMIN.getName()); - } - try(TestRestClient client = cluster.getRestClient(ADDITIONAL_USER_1, ADDITIONAL_PASSWORD_1)) { - client.assertCorrectCredentials(ADDITIONAL_USER_1); - } - } - - @Test - public void shouldCreateUserViaRestApi_failure() { - try(TestRestClient client = cluster.getRestClient(LIMITED_USER)) { - HttpResponse httpResponse = client.putJson(INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_1, String.format(CREATE_USER_BODY, - ADDITIONAL_PASSWORD_1)); - - httpResponse.assertStatusCode(403); - } - } - - @Test - public void shouldAuthenticateAsAdminWithCertificate_positive() { - try(TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - HttpResponse httpResponse = client.get("/_plugins/_security/whoami"); - - httpResponse.assertStatusCode(200); - assertThat(httpResponse.getTextFromJsonBody("/is_admin"), equalTo("true")); - } - } - - @Test - public void shouldAuthenticateAsAdminWithCertificate_negativeSelfSignedCertificate() { - TestCertificates testCertificates = cluster.getTestCertificates(); - try(TestRestClient client = cluster.getRestClient(testCertificates.createSelfSignedCertificate("CN=bond"))) { - HttpResponse httpResponse = client.get("/_plugins/_security/whoami"); - - httpResponse.assertStatusCode(200); - assertThat(httpResponse.getTextFromJsonBody("/is_admin"), equalTo("false")); - } - } - - @Test - public void shouldAuthenticateAsAdminWithCertificate_negativeIncorrectDn() { - TestCertificates testCertificates = cluster.getTestCertificates(); - try(TestRestClient client = cluster.getRestClient(testCertificates.createAdminCertificate("CN=non_admin"))) { - HttpResponse httpResponse = client.get("/_plugins/_security/whoami"); - - httpResponse.assertStatusCode(200); - assertThat(httpResponse.getTextFromJsonBody("/is_admin"), equalTo("false")); - } - } - - @Test - public void shouldCreateUserViaRestApiWhenAdminIsAuthenticatedViaCertificate_positive() { - try(TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { - - HttpResponse httpResponse = client.putJson(INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_2, String.format(CREATE_USER_BODY, - ADDITIONAL_PASSWORD_2)); - - httpResponse.assertStatusCode(201); - } - try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - client.assertCorrectCredentials(USER_ADMIN.getName()); - } - try(TestRestClient client = cluster.getRestClient(ADDITIONAL_USER_2, ADDITIONAL_PASSWORD_2)) { - client.assertCorrectCredentials(ADDITIONAL_USER_2); - } - } - - @Test - public void shouldCreateUserViaRestApiWhenAdminIsAuthenticatedViaCertificate_negative() { - TestCertificates testCertificates = cluster.getTestCertificates(); - try(TestRestClient client = cluster.getRestClient(testCertificates.createSelfSignedCertificate("CN=attacker"))) { - HttpResponse httpResponse = client.putJson(INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_2, String.format(CREATE_USER_BODY, - ADDITIONAL_PASSWORD_2)); - - httpResponse.assertStatusCode(401); - } - } - - @Test - public void shouldStillWorkAfterUpdateOfSecurityConfig() { - List users = new ArrayList<>(cluster.getConfiguredUsers()); - User newUser = new User("new-user"); - users.add(newUser); - - cluster.updateUserConfiguration(users); - - try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - client.assertCorrectCredentials(USER_ADMIN.getName()); - } - try(TestRestClient client = cluster.getRestClient(newUser)) { - client.assertCorrectCredentials(newUser.getName()); - } - } - - @Test - public void shouldAccessIndexWithPlaceholder_positive() { - try(TestRestClient client = cluster.getRestClient(LIMITED_USER)) { - HttpResponse httpResponse = client.get("/" + LIMITED_USER_INDEX + "/_doc/" + ID_1); - - httpResponse.assertStatusCode(200); - } - } - - @Test - public void shouldAccessIndexWithPlaceholder_negative() { - try(TestRestClient client = cluster.getRestClient(LIMITED_USER)) { - HttpResponse httpResponse = client.get("/" + PROHIBITED_INDEX + "/_doc/" + ID_2); - - httpResponse.assertStatusCode(403); - } - } - - @Test - public void shouldUseSecurityAdminTool() throws Exception { - SecurityAdminLauncher securityAdminLauncher = new SecurityAdminLauncher(cluster.getHttpPort(), cluster.getTestCertificates()); - File rolesMapping = configurationDirectory.newFile("roles_mapping.yml"); - ConfigurationFiles.createRoleMappingFile(rolesMapping); - - int exitCode = securityAdminLauncher.updateRoleMappings(rolesMapping); - - assertThat(exitCode, equalTo(0)); - try(TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - Awaitility.await().alias("Waiting for rolemapping 'readall' availability.") - .until(() -> client.get("_plugins/_security/api/rolesmapping/readall").getStatusCode(), equalTo(200)); - } - } - - @Test - public void shouldReloadOnBehalfOfConfigurationFromFile() throws Exception { - SecurityAdminLauncher securityAdminLauncher = new SecurityAdminLauncher(cluster.getHttpPort(), cluster.getTestCertificates()); - File config = configurationDirectory.newFile("config.yml"); - ConfigurationFiles.createConfigFile(config); - int exitCode = securityAdminLauncher.updateConfig(config); - assertThat(exitCode, equalTo(0)); - - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - Awaitility.await() - .until(() -> - { - HttpResponse httpResponse = client.get("_plugins/_security/api/securityconfig"); - JsonNode jsonNode = DefaultObjectMapper.objectMapper.readTree(httpResponse.getBody()); - return jsonNode.get("config").get("dynamic").get("on_behalf_of"); - - }, jsonNode -> jsonNode.get("encryption_key").asText().equals("encryption key") && jsonNode.get("signing_key").asText().equals("signing key") - ); - } - } + private static final User USER_ADMIN = new User("admin").roles(ALL_ACCESS); + private static final User LIMITED_USER = new User("limited-user").roles( + new Role("limited-role").indexPermissions("indices:data/read/search", "indices:data/read/get").on("user-${user.name}") + ); + public static final String LIMITED_USER_INDEX = "user-" + LIMITED_USER.getName(); + public static final String ADDITIONAL_USER_1 = "additional00001"; + public static final String ADDITIONAL_PASSWORD_1 = "user 1 fair password"; + + public static final String ADDITIONAL_USER_2 = "additional2"; + public static final String ADDITIONAL_PASSWORD_2 = "user 2 fair password"; + public static final String CREATE_USER_BODY = "{\"password\": \"%s\",\"opendistro_security_roles\": []}"; + public static final String INTERNAL_USERS_RESOURCE = "_plugins/_security/api/internalusers/"; + public static final String ID_1 = "one"; + public static final String PROHIBITED_INDEX = "prohibited"; + public static final String ID_2 = "two"; + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_ADMIN, LIMITED_USER) + .anonymousAuth(false) + .nodeSettings( + Map.of( + SECURITY_RESTAPI_ROLES_ENABLED, + List.of("user_" + USER_ADMIN.getName() + "__" + ALL_ACCESS.getName()), + SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, + true + ) + ) + .build(); + + @Rule + public TemporaryFolder configurationDirectory = new TemporaryFolder(); + + @BeforeClass + public static void initData() { + try (Client client = cluster.getInternalNodeClient()) { + client.prepareIndex(LIMITED_USER_INDEX).setId(ID_1).setRefreshPolicy(IMMEDIATE).setSource("foo", "bar").get(); + client.prepareIndex(PROHIBITED_INDEX).setId(ID_2).setRefreshPolicy(IMMEDIATE).setSource("three", "four").get(); + } + } + + @Test + public void shouldCreateUserViaRestApi_success() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + HttpResponse httpResponse = client.putJson( + INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_1, + String.format(CREATE_USER_BODY, ADDITIONAL_PASSWORD_1) + ); + + assertThat(httpResponse.getStatusCode(), equalTo(201)); + } + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + client.assertCorrectCredentials(USER_ADMIN.getName()); + } + try (TestRestClient client = cluster.getRestClient(ADDITIONAL_USER_1, ADDITIONAL_PASSWORD_1)) { + client.assertCorrectCredentials(ADDITIONAL_USER_1); + } + } + + @Test + public void shouldCreateUserViaRestApi_failure() { + try (TestRestClient client = cluster.getRestClient(LIMITED_USER)) { + HttpResponse httpResponse = client.putJson( + INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_1, + String.format(CREATE_USER_BODY, ADDITIONAL_PASSWORD_1) + ); + + httpResponse.assertStatusCode(403); + } + } + + @Test + public void shouldAuthenticateAsAdminWithCertificate_positive() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + HttpResponse httpResponse = client.get("/_plugins/_security/whoami"); + + httpResponse.assertStatusCode(200); + assertThat(httpResponse.getTextFromJsonBody("/is_admin"), equalTo("true")); + } + } + + @Test + public void shouldAuthenticateAsAdminWithCertificate_negativeSelfSignedCertificate() { + TestCertificates testCertificates = cluster.getTestCertificates(); + try (TestRestClient client = cluster.getRestClient(testCertificates.createSelfSignedCertificate("CN=bond"))) { + HttpResponse httpResponse = client.get("/_plugins/_security/whoami"); + + httpResponse.assertStatusCode(200); + assertThat(httpResponse.getTextFromJsonBody("/is_admin"), equalTo("false")); + } + } + + @Test + public void shouldAuthenticateAsAdminWithCertificate_negativeIncorrectDn() { + TestCertificates testCertificates = cluster.getTestCertificates(); + try (TestRestClient client = cluster.getRestClient(testCertificates.createAdminCertificate("CN=non_admin"))) { + HttpResponse httpResponse = client.get("/_plugins/_security/whoami"); + + httpResponse.assertStatusCode(200); + assertThat(httpResponse.getTextFromJsonBody("/is_admin"), equalTo("false")); + } + } + + @Test + public void shouldCreateUserViaRestApiWhenAdminIsAuthenticatedViaCertificate_positive() { + try (TestRestClient client = cluster.getRestClient(cluster.getAdminCertificate())) { + + HttpResponse httpResponse = client.putJson( + INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_2, + String.format(CREATE_USER_BODY, ADDITIONAL_PASSWORD_2) + ); + + httpResponse.assertStatusCode(201); + } + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + client.assertCorrectCredentials(USER_ADMIN.getName()); + } + try (TestRestClient client = cluster.getRestClient(ADDITIONAL_USER_2, ADDITIONAL_PASSWORD_2)) { + client.assertCorrectCredentials(ADDITIONAL_USER_2); + } + } + + @Test + public void shouldCreateUserViaRestApiWhenAdminIsAuthenticatedViaCertificate_negative() { + TestCertificates testCertificates = cluster.getTestCertificates(); + try (TestRestClient client = cluster.getRestClient(testCertificates.createSelfSignedCertificate("CN=attacker"))) { + HttpResponse httpResponse = client.putJson( + INTERNAL_USERS_RESOURCE + ADDITIONAL_USER_2, + String.format(CREATE_USER_BODY, ADDITIONAL_PASSWORD_2) + ); + + httpResponse.assertStatusCode(401); + } + } + + @Test + public void shouldStillWorkAfterUpdateOfSecurityConfig() { + List users = new ArrayList<>(cluster.getConfiguredUsers()); + User newUser = new User("new-user"); + users.add(newUser); + + cluster.updateUserConfiguration(users); + + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + client.assertCorrectCredentials(USER_ADMIN.getName()); + } + try (TestRestClient client = cluster.getRestClient(newUser)) { + client.assertCorrectCredentials(newUser.getName()); + } + } + + @Test + public void shouldAccessIndexWithPlaceholder_positive() { + try (TestRestClient client = cluster.getRestClient(LIMITED_USER)) { + HttpResponse httpResponse = client.get("/" + LIMITED_USER_INDEX + "/_doc/" + ID_1); + + httpResponse.assertStatusCode(200); + } + } + + @Test + public void shouldAccessIndexWithPlaceholder_negative() { + try (TestRestClient client = cluster.getRestClient(LIMITED_USER)) { + HttpResponse httpResponse = client.get("/" + PROHIBITED_INDEX + "/_doc/" + ID_2); + + httpResponse.assertStatusCode(403); + } + } + + @Test + public void shouldUseSecurityAdminTool() throws Exception { + SecurityAdminLauncher securityAdminLauncher = new SecurityAdminLauncher(cluster.getHttpPort(), cluster.getTestCertificates()); + File rolesMapping = configurationDirectory.newFile("roles_mapping.yml"); + ConfigurationFiles.createRoleMappingFile(rolesMapping); + + int exitCode = securityAdminLauncher.updateRoleMappings(rolesMapping); + + assertThat(exitCode, equalTo(0)); + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + Awaitility.await() + .alias("Waiting for rolemapping 'readall' availability.") + .until(() -> client.get("_plugins/_security/api/rolesmapping/readall").getStatusCode(), equalTo(200)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/SecurityRolesTests.java b/src/integrationTest/java/org/opensearch/security/SecurityRolesTests.java index c25d28ef12..fcccaf53b3 100644 --- a/src/integrationTest/java/org/opensearch/security/SecurityRolesTests.java +++ b/src/integrationTest/java/org/opensearch/security/SecurityRolesTests.java @@ -32,29 +32,32 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class SecurityRolesTests { - protected final static TestSecurityConfig.User USER_SR = new TestSecurityConfig.User("sr_user").roles( - new Role("abc_ber").indexPermissions("*").on("*").clusterPermissions("*"), - new Role("def_efg").indexPermissions("*").on("*").clusterPermissions("*")); - - @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(true) - .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_SR).build(); - - @Test - public void testSecurityRoles() throws Exception { - try (TestRestClient client = cluster.getRestClient(USER_SR)) { - HttpResponse response = client.getAuthInfo(); - response.assertStatusCode(HttpStatus.SC_OK); - - // Check username - assertThat(response.getTextFromJsonBody("/user_name"), equalTo("sr_user")); - - // Check security roles - assertThat(response.getTextFromJsonBody("/roles/0"), equalTo("user_sr_user__abc_ber")); - assertThat(response.getTextFromJsonBody("/roles/1"), equalTo("user_sr_user__def_efg")); - - } - } + protected final static TestSecurityConfig.User USER_SR = new TestSecurityConfig.User("sr_user").roles( + new Role("abc_ber").indexPermissions("*").on("*").clusterPermissions("*"), + new Role("def_efg").indexPermissions("*").on("*").clusterPermissions("*") + ); + + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .anonymousAuth(true) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_SR) + .build(); + + @Test + public void testSecurityRoles() throws Exception { + try (TestRestClient client = cluster.getRestClient(USER_SR)) { + HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(HttpStatus.SC_OK); + + // Check username + assertThat(response.getTextFromJsonBody("/user_name"), equalTo("sr_user")); + + // Check security roles + assertThat(response.getTextFromJsonBody("/roles/0"), equalTo("user_sr_user__abc_ber")); + assertThat(response.getTextFromJsonBody("/roles/1"), equalTo("user_sr_user__def_efg")); + + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/SnapshotSteps.java b/src/integrationTest/java/org/opensearch/security/SnapshotSteps.java index f346bedd31..28aa6abd43 100644 --- a/src/integrationTest/java/org/opensearch/security/SnapshotSteps.java +++ b/src/integrationTest/java/org/opensearch/security/SnapshotSteps.java @@ -33,55 +33,63 @@ class SnapshotSteps { - private final SnapshotClient snapshotClient; + private final SnapshotClient snapshotClient; - public SnapshotSteps(RestHighLevelClient restHighLevelClient) { - this.snapshotClient = requireNonNull(restHighLevelClient, "Rest high level client is required.").snapshot(); - } + public SnapshotSteps(RestHighLevelClient restHighLevelClient) { + this.snapshotClient = requireNonNull(restHighLevelClient, "Rest high level client is required.").snapshot(); + } - // CS-SUPPRESS-SINGLE: RegexpSingleline It is not possible to use phrase "cluster manager" instead of master here - public org.opensearch.action.support.master.AcknowledgedResponse createSnapshotRepository(String repositoryName, String snapshotDirPath, String type) - // CS-ENFORCE-SINGLE - throws IOException { - PutRepositoryRequest createRepositoryRequest = new PutRepositoryRequest().name(repositoryName).type(type) - .settings(Map.of("location", snapshotDirPath)); - return snapshotClient.createRepository(createRepositoryRequest, DEFAULT); - } + // CS-SUPPRESS-SINGLE: RegexpSingleline It is not possible to use phrase "cluster manager" instead of master here + public org.opensearch.action.support.master.AcknowledgedResponse createSnapshotRepository( + String repositoryName, + String snapshotDirPath, + String type + ) + // CS-ENFORCE-SINGLE + throws IOException { + PutRepositoryRequest createRepositoryRequest = new PutRepositoryRequest().name(repositoryName) + .type(type) + .settings(Map.of("location", snapshotDirPath)); + return snapshotClient.createRepository(createRepositoryRequest, DEFAULT); + } - public CreateSnapshotResponse createSnapshot(String repositoryName, String snapshotName, String...indices) throws IOException { - CreateSnapshotRequest createSnapshotRequest = new CreateSnapshotRequest(repositoryName, snapshotName) - .indices(indices); - return snapshotClient.create(createSnapshotRequest, DEFAULT); - } + public CreateSnapshotResponse createSnapshot(String repositoryName, String snapshotName, String... indices) throws IOException { + CreateSnapshotRequest createSnapshotRequest = new CreateSnapshotRequest(repositoryName, snapshotName).indices(indices); + return snapshotClient.create(createSnapshotRequest, DEFAULT); + } - public void waitForSnapshotCreation(String repositoryName, String snapshotName) { - GetSnapshotsRequest getSnapshotsRequest = new GetSnapshotsRequest(repositoryName, new String[] { snapshotName }); - Awaitility.await().alias("wait for snapshot creation").ignoreExceptions().until(() -> { - GetSnapshotsResponse snapshotsResponse = snapshotClient.get(getSnapshotsRequest, DEFAULT); - SnapshotInfo snapshotInfo = snapshotsResponse.getSnapshots().get(0); - return SnapshotState.SUCCESS.equals(snapshotInfo.state()); - }); - } + public void waitForSnapshotCreation(String repositoryName, String snapshotName) { + GetSnapshotsRequest getSnapshotsRequest = new GetSnapshotsRequest(repositoryName, new String[] { snapshotName }); + Awaitility.await().alias("wait for snapshot creation").ignoreExceptions().until(() -> { + GetSnapshotsResponse snapshotsResponse = snapshotClient.get(getSnapshotsRequest, DEFAULT); + SnapshotInfo snapshotInfo = snapshotsResponse.getSnapshots().get(0); + return SnapshotState.SUCCESS.equals(snapshotInfo.state()); + }); + } - //CS-SUPPRESS-SINGLE: RegexpSingleline It is not possible to use phrase "cluster manager" instead of master here - public org.opensearch.action.support.master.AcknowledgedResponse deleteSnapshotRepository(String repositoryName) throws IOException { - //CS-ENFORCE-SINGLE - DeleteRepositoryRequest request = new DeleteRepositoryRequest(repositoryName); - return snapshotClient.deleteRepository(request, DEFAULT); - } + // CS-SUPPRESS-SINGLE: RegexpSingleline It is not possible to use phrase "cluster manager" instead of master here + public org.opensearch.action.support.master.AcknowledgedResponse deleteSnapshotRepository(String repositoryName) throws IOException { + // CS-ENFORCE-SINGLE + DeleteRepositoryRequest request = new DeleteRepositoryRequest(repositoryName); + return snapshotClient.deleteRepository(request, DEFAULT); + } - //CS-SUPPRESS-SINGLE: RegexpSingleline It is not possible to use phrase "cluster manager" instead of master here - public org.opensearch.action.support.master.AcknowledgedResponse deleteSnapshot(String repositoryName, String snapshotName) throws IOException { - //CS-ENFORCE-SINGLE - return snapshotClient.delete(new DeleteSnapshotRequest(repositoryName, snapshotName), DEFAULT); - } + // CS-SUPPRESS-SINGLE: RegexpSingleline It is not possible to use phrase "cluster manager" instead of master here + public org.opensearch.action.support.master.AcknowledgedResponse deleteSnapshot(String repositoryName, String snapshotName) + throws IOException { + // CS-ENFORCE-SINGLE + return snapshotClient.delete(new DeleteSnapshotRequest(repositoryName, snapshotName), DEFAULT); + } - public RestoreSnapshotResponse restoreSnapshot( - String repositoryName, String snapshotName, String renamePattern, - String renameReplacement) throws IOException { - RestoreSnapshotRequest restoreSnapshotRequest = new RestoreSnapshotRequest(repositoryName, snapshotName) - .renamePattern(renamePattern) - .renameReplacement(renameReplacement); - return snapshotClient.restore(restoreSnapshotRequest, DEFAULT); - } + public RestoreSnapshotResponse restoreSnapshot( + String repositoryName, + String snapshotName, + String renamePattern, + String renameReplacement + ) throws IOException { + RestoreSnapshotRequest restoreSnapshotRequest = new RestoreSnapshotRequest(repositoryName, snapshotName).renamePattern( + renamePattern + ).renameReplacement(renameReplacement); + return snapshotClient.restore(restoreSnapshotRequest, DEFAULT); + } } diff --git a/src/integrationTest/java/org/opensearch/security/Song.java b/src/integrationTest/java/org/opensearch/security/Song.java index c0e5749d29..b7e6c4ef05 100644 --- a/src/integrationTest/java/org/opensearch/security/Song.java +++ b/src/integrationTest/java/org/opensearch/security/Song.java @@ -14,90 +14,85 @@ public class Song { - public static final String FIELD_TITLE = "title"; - public static final String FIELD_ARTIST = "artist"; - public static final String FIELD_LYRICS = "lyrics"; - public static final String FIELD_STARS = "stars"; - public static final String FIELD_GENRE = "genre"; - public static final String ARTIST_FIRST = "First artist"; - public static final String ARTIST_STRING = "String"; - public static final String ARTIST_TWINS = "Twins"; - public static final String TITLE_MAGNUM_OPUS = "Magnum Opus"; - public static final String TITLE_SONG_1_PLUS_1 = "Song 1+1"; - public static final String TITLE_NEXT_SONG = "Next song"; - public static final String ARTIST_NO = "No!"; - public static final String TITLE_POISON = "Poison"; - - public static final String ARTIST_YES = "yes"; - - public static final String TITLE_AFFIRMATIVE = "Affirmative"; - - public static final String ARTIST_UNKNOWN = "unknown"; - public static final String TITLE_CONFIDENTIAL = "confidential"; - - public static final String LYRICS_1 = "Very deep subject"; - public static final String LYRICS_2 = "Once upon a time"; - public static final String LYRICS_3 = "giant nonsense"; - public static final String LYRICS_4 = "Much too much"; - public static final String LYRICS_5 = "Little to little"; - public static final String LYRICS_6 = "confidential secret classified"; - - public static final String GENRE_ROCK = "rock"; - public static final String GENRE_JAZZ = "jazz"; - public static final String GENRE_BLUES = "blues"; - - public static final String QUERY_TITLE_NEXT_SONG = FIELD_TITLE + ":" + "\"" + TITLE_NEXT_SONG + "\""; - public static final String QUERY_TITLE_POISON = FIELD_TITLE + ":" + TITLE_POISON; - public static final String QUERY_TITLE_MAGNUM_OPUS = FIELD_TITLE + ":" + TITLE_MAGNUM_OPUS; - - public static final Song[] SONGS = { - new Song(ARTIST_FIRST, TITLE_MAGNUM_OPUS ,LYRICS_1, 1, GENRE_ROCK), - new Song(ARTIST_STRING, TITLE_SONG_1_PLUS_1, LYRICS_2, 2, GENRE_BLUES), - new Song(ARTIST_TWINS, TITLE_NEXT_SONG, LYRICS_3, 3, GENRE_JAZZ), - new Song(ARTIST_NO, TITLE_POISON, LYRICS_4, 4, GENRE_ROCK), - new Song(ARTIST_YES, TITLE_AFFIRMATIVE,LYRICS_5, 5, GENRE_BLUES), - new Song(ARTIST_UNKNOWN, TITLE_CONFIDENTIAL, LYRICS_6, 6, GENRE_JAZZ) - }; - - private final String artist; - private final String title; - private final String lyrics; - private final Integer stars; - private final String genre; - - public Song(String artist, String title, String lyrics, Integer stars, String genre) { - this.artist = Objects.requireNonNull(artist, "Artist is required"); - this.title = Objects.requireNonNull(title, "Title is required"); - this.lyrics = Objects.requireNonNull(lyrics, "Lyrics is required"); - this.stars = Objects.requireNonNull(stars, "Stars field is required"); - this.genre = Objects.requireNonNull(genre, "Genre field is required"); - } - - public String getArtist() { - return artist; - } - - public String getTitle() { - return title; - } - - public String getLyrics() { - return lyrics; - } - - public Integer getStars() { - return stars; - } - - public String getGenre() { - return genre; - } - - public Map asMap() { - return Map.of(FIELD_ARTIST, artist, - FIELD_TITLE, title, - FIELD_LYRICS, lyrics, - FIELD_STARS, stars, - FIELD_GENRE, genre); - } + public static final String FIELD_TITLE = "title"; + public static final String FIELD_ARTIST = "artist"; + public static final String FIELD_LYRICS = "lyrics"; + public static final String FIELD_STARS = "stars"; + public static final String FIELD_GENRE = "genre"; + public static final String ARTIST_FIRST = "First artist"; + public static final String ARTIST_STRING = "String"; + public static final String ARTIST_TWINS = "Twins"; + public static final String TITLE_MAGNUM_OPUS = "Magnum Opus"; + public static final String TITLE_SONG_1_PLUS_1 = "Song 1+1"; + public static final String TITLE_NEXT_SONG = "Next song"; + public static final String ARTIST_NO = "No!"; + public static final String TITLE_POISON = "Poison"; + + public static final String ARTIST_YES = "yes"; + + public static final String TITLE_AFFIRMATIVE = "Affirmative"; + + public static final String ARTIST_UNKNOWN = "unknown"; + public static final String TITLE_CONFIDENTIAL = "confidential"; + + public static final String LYRICS_1 = "Very deep subject"; + public static final String LYRICS_2 = "Once upon a time"; + public static final String LYRICS_3 = "giant nonsense"; + public static final String LYRICS_4 = "Much too much"; + public static final String LYRICS_5 = "Little to little"; + public static final String LYRICS_6 = "confidential secret classified"; + + public static final String GENRE_ROCK = "rock"; + public static final String GENRE_JAZZ = "jazz"; + public static final String GENRE_BLUES = "blues"; + + public static final String QUERY_TITLE_NEXT_SONG = FIELD_TITLE + ":" + "\"" + TITLE_NEXT_SONG + "\""; + public static final String QUERY_TITLE_POISON = FIELD_TITLE + ":" + TITLE_POISON; + public static final String QUERY_TITLE_MAGNUM_OPUS = FIELD_TITLE + ":" + TITLE_MAGNUM_OPUS; + + public static final Song[] SONGS = { + new Song(ARTIST_FIRST, TITLE_MAGNUM_OPUS, LYRICS_1, 1, GENRE_ROCK), + new Song(ARTIST_STRING, TITLE_SONG_1_PLUS_1, LYRICS_2, 2, GENRE_BLUES), + new Song(ARTIST_TWINS, TITLE_NEXT_SONG, LYRICS_3, 3, GENRE_JAZZ), + new Song(ARTIST_NO, TITLE_POISON, LYRICS_4, 4, GENRE_ROCK), + new Song(ARTIST_YES, TITLE_AFFIRMATIVE, LYRICS_5, 5, GENRE_BLUES), + new Song(ARTIST_UNKNOWN, TITLE_CONFIDENTIAL, LYRICS_6, 6, GENRE_JAZZ) }; + + private final String artist; + private final String title; + private final String lyrics; + private final Integer stars; + private final String genre; + + public Song(String artist, String title, String lyrics, Integer stars, String genre) { + this.artist = Objects.requireNonNull(artist, "Artist is required"); + this.title = Objects.requireNonNull(title, "Title is required"); + this.lyrics = Objects.requireNonNull(lyrics, "Lyrics is required"); + this.stars = Objects.requireNonNull(stars, "Stars field is required"); + this.genre = Objects.requireNonNull(genre, "Genre field is required"); + } + + public String getArtist() { + return artist; + } + + public String getTitle() { + return title; + } + + public String getLyrics() { + return lyrics; + } + + public Integer getStars() { + return stars; + } + + public String getGenre() { + return genre; + } + + public Map asMap() { + return Map.of(FIELD_ARTIST, artist, FIELD_TITLE, title, FIELD_LYRICS, lyrics, FIELD_STARS, stars, FIELD_GENRE, genre); + } } diff --git a/src/integrationTest/java/org/opensearch/security/SslOnlyTests.java b/src/integrationTest/java/org/opensearch/security/SslOnlyTests.java index b258f0fed2..25feffb2b4 100644 --- a/src/integrationTest/java/org/opensearch/security/SslOnlyTests.java +++ b/src/integrationTest/java/org/opensearch/security/SslOnlyTests.java @@ -33,37 +33,37 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class SslOnlyTests { + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .anonymousAuth(false) + .loadConfigurationIntoIndex(false) + .nodeSettings(Map.of(ConfigConstants.SECURITY_SSL_ONLY, true)) + .sslOnly(true) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .build(); - @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(false) - .loadConfigurationIntoIndex(false) - .nodeSettings(Map.of(ConfigConstants.SECURITY_SSL_ONLY, true)) - .sslOnly(true) - .authc(AUTHC_HTTPBASIC_INTERNAL).build(); + @Test + public void shouldNotLoadSecurityPluginResources() { + try (TestRestClient client = cluster.getRestClient()) { - @Test - public void shouldNotLoadSecurityPluginResources() { - try(TestRestClient client = cluster.getRestClient()) { + HttpResponse response = client.getAuthInfo(); - HttpResponse response = client.getAuthInfo(); + // in SSL only mode the security plugin does not register a handler for resource /_plugins/_security/whoami. Therefore error + // response is returned. + response.assertStatusCode(400); + } + } - // in SSL only mode the security plugin does not register a handler for resource /_plugins/_security/whoami. Therefore error - // response is returned. - response.assertStatusCode(400); - } - } + @Test + public void shouldGetIndicesWithoutAuthentication() { + try (TestRestClient client = cluster.getRestClient()) { - @Test - public void shouldGetIndicesWithoutAuthentication() { - try(TestRestClient client = cluster.getRestClient()) { + // request does not contains credential + HttpResponse response = client.get("/_cat/indices"); - // request does not contains credential - HttpResponse response = client.get("/_cat/indices"); - - // successful response is returned because the security plugin in SSL only mode - // does not perform authentication and authorization - response.assertStatusCode(200); - } - } + // successful response is returned because the security plugin in SSL only mode + // does not perform authentication and authorization + response.assertStatusCode(200); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/TlsTests.java b/src/integrationTest/java/org/opensearch/security/TlsTests.java index 7a57cb57b8..de362a544e 100644 --- a/src/integrationTest/java/org/opensearch/security/TlsTests.java +++ b/src/integrationTest/java/org/opensearch/security/TlsTests.java @@ -50,55 +50,57 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class TlsTests { - private static final User USER_ADMIN = new User("admin").roles(ALL_ACCESS); - - public static final String SUPPORTED_CIPHER_SUIT = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; - public static final String NOT_SUPPORTED_CIPHER_SUITE = "TLS_RSA_WITH_AES_128_CBC_SHA"; - public static final String AUTH_INFO_ENDPOINT = "/_opendistro/_security/authinfo?pretty"; - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(false) - .nodeSettings(Map.of(SECURITY_SSL_HTTP_ENABLED_CIPHERS, List.of(SUPPORTED_CIPHER_SUIT))) - .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN) - .audit(new AuditConfiguration(true) - .compliance(new AuditCompliance().enabled(true)) - .filters(new AuditFilters().enabledRest(true).enabledTransport(true)) - ).build(); - - @Rule - public AuditLogsRule auditLogsRule = new AuditLogsRule(); - - @Test - public void shouldCreateAuditOnIncomingNonTlsConnection() throws IOException { - try(CloseableHttpClient httpClient = HttpClients.createDefault()) { - HttpGet request = new HttpGet("http://localhost:" + cluster.getHttpPort()); - - assertThatThrownBy(() -> httpClient.execute(request), instanceOf(NoHttpResponseException.class)); - } - auditLogsRule.assertAtLeast(1, auditPredicate(AuditCategory.SSL_EXCEPTION).withLayer(REST)); - } - - @Test - public void shouldSupportClientCipherSuite_positive() throws IOException { - try(CloseableHttpClient client = cluster.getClosableHttpClient(new String[] { SUPPORTED_CIPHER_SUIT })) { - HttpGet httpGet = new HttpGet("https://localhost:" + cluster.getHttpPort() + AUTH_INFO_ENDPOINT); - httpGet.addHeader(getBasicAuthHeader(USER_ADMIN.getName(), USER_ADMIN.getPassword())); - - try(CloseableHttpResponse response = client.execute(httpGet)) { - - int responseStatusCode = response.getCode(); - assertThat(responseStatusCode, equalTo(200)); - } - } - } - - @Test - public void shouldSupportClientCipherSuite_negative() throws IOException { - try(CloseableHttpClient client = cluster.getClosableHttpClient(new String[]{ NOT_SUPPORTED_CIPHER_SUITE })) { - HttpGet httpGet = new HttpGet("https://localhost:" + cluster.getHttpPort() + AUTH_INFO_ENDPOINT); - - assertThatThrownBy(() -> client.execute(httpGet), instanceOf(SSLHandshakeException.class)); - } - } + private static final User USER_ADMIN = new User("admin").roles(ALL_ACCESS); + + public static final String SUPPORTED_CIPHER_SUIT = "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"; + public static final String NOT_SUPPORTED_CIPHER_SUITE = "TLS_RSA_WITH_AES_128_CBC_SHA"; + public static final String AUTH_INFO_ENDPOINT = "/_opendistro/_security/authinfo?pretty"; + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .anonymousAuth(false) + .nodeSettings(Map.of(SECURITY_SSL_HTTP_ENABLED_CIPHERS, List.of(SUPPORTED_CIPHER_SUIT))) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_ADMIN) + .audit( + new AuditConfiguration(true).compliance(new AuditCompliance().enabled(true)) + .filters(new AuditFilters().enabledRest(true).enabledTransport(true)) + ) + .build(); + + @Rule + public AuditLogsRule auditLogsRule = new AuditLogsRule(); + + @Test + public void shouldCreateAuditOnIncomingNonTlsConnection() throws IOException { + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + HttpGet request = new HttpGet("http://localhost:" + cluster.getHttpPort()); + + assertThatThrownBy(() -> httpClient.execute(request), instanceOf(NoHttpResponseException.class)); + } + auditLogsRule.assertAtLeast(1, auditPredicate(AuditCategory.SSL_EXCEPTION).withLayer(REST)); + } + + @Test + public void shouldSupportClientCipherSuite_positive() throws IOException { + try (CloseableHttpClient client = cluster.getClosableHttpClient(new String[] { SUPPORTED_CIPHER_SUIT })) { + HttpGet httpGet = new HttpGet("https://localhost:" + cluster.getHttpPort() + AUTH_INFO_ENDPOINT); + httpGet.addHeader(getBasicAuthHeader(USER_ADMIN.getName(), USER_ADMIN.getPassword())); + + try (CloseableHttpResponse response = client.execute(httpGet)) { + + int responseStatusCode = response.getCode(); + assertThat(responseStatusCode, equalTo(200)); + } + } + } + + @Test + public void shouldSupportClientCipherSuite_negative() throws IOException { + try (CloseableHttpClient client = cluster.getClosableHttpClient(new String[] { NOT_SUPPORTED_CIPHER_SUITE })) { + HttpGet httpGet = new HttpGet("https://localhost:" + cluster.getHttpPort() + AUTH_INFO_ENDPOINT); + + assertThatThrownBy(() -> client.execute(httpGet), instanceOf(SSLHandshakeException.class)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java b/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java index e0e9d5beb6..2da3446e75 100644 --- a/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java +++ b/src/integrationTest/java/org/opensearch/security/UserBruteForceAttacksPreventionTests.java @@ -35,91 +35,99 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class UserBruteForceAttacksPreventionTests { - private static final User USER_1 = new User("simple-user-1").roles(ALL_ACCESS); - private static final User USER_2 = new User("simple-user-2").roles(ALL_ACCESS); - private static final User USER_3 = new User("simple-user-3").roles(ALL_ACCESS); - private static final User USER_4 = new User("simple-user-4").roles(ALL_ACCESS); - private static final User USER_5 = new User("simple-user-5").roles(ALL_ACCESS); - - public static final int ALLOWED_TRIES = 3; - public static final int TIME_WINDOW_SECONDS = 3; - private static final AuthFailureListeners listener = new AuthFailureListeners() - .addRateLimit(new RateLimiting("internal_authentication_backend_limiting").type("username").authenticationBackend("intern") - .allowedTries(ALLOWED_TRIES).timeWindowSeconds(TIME_WINDOW_SECONDS).blockExpirySeconds(2).maxBlockedClients(500) - .maxTrackedClients(500)); - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false).authFailureListeners(listener) - .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_1, USER_2, USER_3, USER_4, USER_5).build(); - - @Rule - public LogsRule logsRule = new LogsRule("org.opensearch.security.auth.BackendRegistry"); - - @Test - public void shouldAuthenticateUserWhenBlockadeIsNotActive() { - try(TestRestClient client = cluster.getRestClient(USER_1)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_OK); - } - } - - @Test - public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsEqualToLimit() { - authenticateUserWithIncorrectPassword(USER_2, ALLOWED_TRIES); - try(TestRestClient client = cluster.getRestClient(USER_2)) { - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_UNAUTHORIZED); - } - //Rejecting REST request because of blocked user: - logsRule.assertThatContain("Rejecting REST request because of blocked user: " + USER_2.getName()); - } - - @Test - public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsGreaterThanLimit() { - authenticateUserWithIncorrectPassword(USER_3, ALLOWED_TRIES * 2); - try(TestRestClient client = cluster.getRestClient(USER_3)) { - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_UNAUTHORIZED); - } - logsRule.assertThatContain("Rejecting REST request because of blocked user: " + USER_3.getName()); - } - - @Test - public void shouldNotBlockUserWhenNumberOfLoginAttemptIsBelowLimit() { - authenticateUserWithIncorrectPassword(USER_4, ALLOWED_TRIES - 1); - try(TestRestClient client = cluster.getRestClient(USER_4)) { - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_OK); - } - } - - @Test - public void shouldReleaseLock() throws InterruptedException { - authenticateUserWithIncorrectPassword(USER_5, ALLOWED_TRIES); - try(TestRestClient client = cluster.getRestClient(USER_5)) { - HttpResponse response = client.getAuthInfo(); - response.assertStatusCode(SC_UNAUTHORIZED); - TimeUnit.SECONDS.sleep(TIME_WINDOW_SECONDS); - - response = client.getAuthInfo(); - - response.assertStatusCode(SC_OK); - } - logsRule.assertThatContain("Rejecting REST request because of blocked user: " + USER_5.getName()); - } - - private static void authenticateUserWithIncorrectPassword(User user, int numberOfAttempts) { - try(TestRestClient client = cluster.getRestClient(user.getName(), "incorrect password")) { - for(int i = 0; i < numberOfAttempts; ++i) { - HttpResponse response = client.getAuthInfo(); - response.assertStatusCode(SC_UNAUTHORIZED); - } - } - } + private static final User USER_1 = new User("simple-user-1").roles(ALL_ACCESS); + private static final User USER_2 = new User("simple-user-2").roles(ALL_ACCESS); + private static final User USER_3 = new User("simple-user-3").roles(ALL_ACCESS); + private static final User USER_4 = new User("simple-user-4").roles(ALL_ACCESS); + private static final User USER_5 = new User("simple-user-5").roles(ALL_ACCESS); + + public static final int ALLOWED_TRIES = 3; + public static final int TIME_WINDOW_SECONDS = 3; + private static final AuthFailureListeners listener = new AuthFailureListeners().addRateLimit( + new RateLimiting("internal_authentication_backend_limiting").type("username") + .authenticationBackend("intern") + .allowedTries(ALLOWED_TRIES) + .timeWindowSeconds(TIME_WINDOW_SECONDS) + .blockExpirySeconds(2) + .maxBlockedClients(500) + .maxTrackedClients(500) + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authFailureListeners(listener) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_1, USER_2, USER_3, USER_4, USER_5) + .build(); + + @Rule + public LogsRule logsRule = new LogsRule("org.opensearch.security.auth.BackendRegistry"); + + @Test + public void shouldAuthenticateUserWhenBlockadeIsNotActive() { + try (TestRestClient client = cluster.getRestClient(USER_1)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + } + + @Test + public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsEqualToLimit() { + authenticateUserWithIncorrectPassword(USER_2, ALLOWED_TRIES); + try (TestRestClient client = cluster.getRestClient(USER_2)) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + } + // Rejecting REST request because of blocked user: + logsRule.assertThatContain("Rejecting REST request because of blocked user: " + USER_2.getName()); + } + + @Test + public void shouldBlockUserWhenNumberOfFailureLoginAttemptIsGreaterThanLimit() { + authenticateUserWithIncorrectPassword(USER_3, ALLOWED_TRIES * 2); + try (TestRestClient client = cluster.getRestClient(USER_3)) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + } + logsRule.assertThatContain("Rejecting REST request because of blocked user: " + USER_3.getName()); + } + + @Test + public void shouldNotBlockUserWhenNumberOfLoginAttemptIsBelowLimit() { + authenticateUserWithIncorrectPassword(USER_4, ALLOWED_TRIES - 1); + try (TestRestClient client = cluster.getRestClient(USER_4)) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + } + + @Test + public void shouldReleaseLock() throws InterruptedException { + authenticateUserWithIncorrectPassword(USER_5, ALLOWED_TRIES); + try (TestRestClient client = cluster.getRestClient(USER_5)) { + HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(SC_UNAUTHORIZED); + TimeUnit.SECONDS.sleep(TIME_WINDOW_SECONDS); + + response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + logsRule.assertThatContain("Rejecting REST request because of blocked user: " + USER_5.getName()); + } + + private static void authenticateUserWithIncorrectPassword(User user, int numberOfAttempts) { + try (TestRestClient client = cluster.getRestClient(user.getName(), "incorrect password")) { + for (int i = 0; i < numberOfAttempts; ++i) { + HttpResponse response = client.getAuthInfo(); + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/AnonymousAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/AnonymousAuthenticationTest.java index d50ed081c8..b1c13aeedc 100644 --- a/src/integrationTest/java/org/opensearch/security/http/AnonymousAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/AnonymousAuthenticationTest.java @@ -34,95 +34,96 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class AnonymousAuthenticationTest { - private static final String DEFAULT_ANONYMOUS_USER_NAME = "opendistro_security_anonymous"; - private static final String DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME = "opendistro_security_anonymous_backendrole"; - - /** - * Custom role assigned to the anonymous user via {@link #ANONYMOUS_USER_CUSTOM_ROLE_MAPPING} - */ - private static final TestSecurityConfig.Role ANONYMOUS_USER_CUSTOM_ROLE = new TestSecurityConfig.Role("anonymous_user_custom_role"); - - /** - * Maps {@link #ANONYMOUS_USER_CUSTOM_ROLE} to {@link #DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME} - */ - private static final RolesMapping ANONYMOUS_USER_CUSTOM_ROLE_MAPPING = new RolesMapping(ANONYMOUS_USER_CUSTOM_ROLE) - .backendRoles(DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME); - - /** - * User who is stored in the internal user database and can authenticate - */ - private static final TestSecurityConfig.User EXISTING_USER = new TestSecurityConfig.User("existing_user") - .roles(new TestSecurityConfig.Role("existing_user")); - - /** - * User who is not stored in the internal user database and can not authenticate - */ - private static final TestSecurityConfig.User NOT_EXISTING_USER = new TestSecurityConfig.User("not_existing_user") - .roles(new TestSecurityConfig.Role("not_existing_user")); - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.SINGLENODE) - .anonymousAuth(true) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(EXISTING_USER) - .roles(ANONYMOUS_USER_CUSTOM_ROLE) - .rolesMapping(ANONYMOUS_USER_CUSTOM_ROLE_MAPPING) - .build(); - - private static final String USER_NAME_POINTER = "/user_name"; - private static final String BACKEND_ROLES_POINTER = "/backend_roles"; - private static final String ROLES_POINTER = "/roles"; - - - @Test - public void shouldAuthenticate_positive_anonymousUser() { - try(TestRestClient client = cluster.getRestClient()){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - - String username = response.getTextFromJsonBody(USER_NAME_POINTER); - assertThat(username, equalTo(DEFAULT_ANONYMOUS_USER_NAME)); - - List backendRoles = response.getTextArrayFromJsonBody(BACKEND_ROLES_POINTER); - assertThat(backendRoles, hasSize(1)); - assertThat(backendRoles, contains(DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME)); - - List roles = response.getTextArrayFromJsonBody(ROLES_POINTER); - assertThat(roles, hasSize(1)); - assertThat(roles, contains(ANONYMOUS_USER_CUSTOM_ROLE.getName())); - } - } - - @Test - public void shouldAuthenticate_positive_existingUser() { - try(TestRestClient client = cluster.getRestClient(EXISTING_USER)){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - - String username = response.getTextFromJsonBody(USER_NAME_POINTER); - assertThat(username, equalTo(EXISTING_USER.getName())); - - List backendRoles = response.getTextArrayFromJsonBody(BACKEND_ROLES_POINTER); - assertThat(backendRoles, hasSize(0)); - - List roles = response.getTextArrayFromJsonBody(ROLES_POINTER); - assertThat(roles, hasSize(EXISTING_USER.getRoleNames().size())); - assertThat(roles, containsInAnyOrder(EXISTING_USER.getRoleNames().toArray())); - } - } - - @Test - public void shouldAuthenticate_negative_notExistingUser() { - try(TestRestClient client = cluster.getRestClient(NOT_EXISTING_USER)){ - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - } - } + private static final String DEFAULT_ANONYMOUS_USER_NAME = "opendistro_security_anonymous"; + private static final String DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME = "opendistro_security_anonymous_backendrole"; + + /** + * Custom role assigned to the anonymous user via {@link #ANONYMOUS_USER_CUSTOM_ROLE_MAPPING} + */ + private static final TestSecurityConfig.Role ANONYMOUS_USER_CUSTOM_ROLE = new TestSecurityConfig.Role("anonymous_user_custom_role"); + + /** + * Maps {@link #ANONYMOUS_USER_CUSTOM_ROLE} to {@link #DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME} + */ + private static final RolesMapping ANONYMOUS_USER_CUSTOM_ROLE_MAPPING = new RolesMapping(ANONYMOUS_USER_CUSTOM_ROLE).backendRoles( + DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME + ); + + /** + * User who is stored in the internal user database and can authenticate + */ + private static final TestSecurityConfig.User EXISTING_USER = new TestSecurityConfig.User("existing_user").roles( + new TestSecurityConfig.Role("existing_user") + ); + + /** + * User who is not stored in the internal user database and can not authenticate + */ + private static final TestSecurityConfig.User NOT_EXISTING_USER = new TestSecurityConfig.User("not_existing_user").roles( + new TestSecurityConfig.Role("not_existing_user") + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(true) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(EXISTING_USER) + .roles(ANONYMOUS_USER_CUSTOM_ROLE) + .rolesMapping(ANONYMOUS_USER_CUSTOM_ROLE_MAPPING) + .build(); + + private static final String USER_NAME_POINTER = "/user_name"; + private static final String BACKEND_ROLES_POINTER = "/backend_roles"; + private static final String ROLES_POINTER = "/roles"; + + @Test + public void shouldAuthenticate_positive_anonymousUser() { + try (TestRestClient client = cluster.getRestClient()) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + + String username = response.getTextFromJsonBody(USER_NAME_POINTER); + assertThat(username, equalTo(DEFAULT_ANONYMOUS_USER_NAME)); + + List backendRoles = response.getTextArrayFromJsonBody(BACKEND_ROLES_POINTER); + assertThat(backendRoles, hasSize(1)); + assertThat(backendRoles, contains(DEFAULT_ANONYMOUS_USER_BACKEND_ROLE_NAME)); + + List roles = response.getTextArrayFromJsonBody(ROLES_POINTER); + assertThat(roles, hasSize(1)); + assertThat(roles, contains(ANONYMOUS_USER_CUSTOM_ROLE.getName())); + } + } + + @Test + public void shouldAuthenticate_positive_existingUser() { + try (TestRestClient client = cluster.getRestClient(EXISTING_USER)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + + String username = response.getTextFromJsonBody(USER_NAME_POINTER); + assertThat(username, equalTo(EXISTING_USER.getName())); + + List backendRoles = response.getTextArrayFromJsonBody(BACKEND_ROLES_POINTER); + assertThat(backendRoles, hasSize(0)); + + List roles = response.getTextArrayFromJsonBody(ROLES_POINTER); + assertThat(roles, hasSize(EXISTING_USER.getRoleNames().size())); + assertThat(roles, containsInAnyOrder(EXISTING_USER.getRoleNames().toArray())); + } + } + + @Test + public void shouldAuthenticate_negative_notExistingUser() { + try (TestRestClient client = cluster.getRestClient(NOT_EXISTING_USER)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/AuthInfo.java b/src/integrationTest/java/org/opensearch/security/http/AuthInfo.java index cb08c2f36d..53ea6ab859 100644 --- a/src/integrationTest/java/org/opensearch/security/http/AuthInfo.java +++ b/src/integrationTest/java/org/opensearch/security/http/AuthInfo.java @@ -17,14 +17,14 @@ @JsonIgnoreProperties(ignoreUnknown = true) class AuthInfo { - private final List customAttributeNames; + private final List customAttributeNames; - @ConstructorProperties("custom_attribute_names") - public AuthInfo(List customAttributeNames) { - this.customAttributeNames = customAttributeNames; - } + @ConstructorProperties("custom_attribute_names") + public AuthInfo(List customAttributeNames) { + this.customAttributeNames = customAttributeNames; + } - public List getCustomAttributeNames() { - return customAttributeNames; - } + public List getCustomAttributeNames() { + return customAttributeNames; + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java b/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java index dafedcdf38..3f4ada4c68 100644 --- a/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/BasicAuthTests.java @@ -37,110 +37,110 @@ @RunWith(com.carrotsearch.randomizedtesting.RandomizedRunner.class) @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class BasicAuthTests { - static final User TEST_USER = new User("test_user").password("s3cret"); - - public static final String CUSTOM_ATTRIBUTE_NAME = "superhero"; - static final User SUPER_USER = new User("super-user").password("super-password") - .attr(CUSTOM_ATTRIBUTE_NAME, true); - public static final String NOT_EXISTING_USER = "not-existing-user"; - public static final String INVALID_PASSWORD = "secret-password"; - - public static final AuthcDomain AUTHC_DOMAIN = new AuthcDomain("basic", 0) - .httpAuthenticatorWithChallenge("basic").backend("internal"); - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .authc(AUTHC_DOMAIN).users(TEST_USER, SUPER_USER).build(); - - @Test - public void shouldRespondWith401WhenUserDoesNotExist() { - try (TestRestClient client = cluster.getRestClient(NOT_EXISTING_USER, INVALID_PASSWORD)) { - HttpResponse response = client.getAuthInfo(); - - assertThat(response, is(notNullValue())); - response.assertStatusCode(SC_UNAUTHORIZED); - } - } - - @Test - public void shouldRespondWith401WhenUserNameIsIncorrect() { - try (TestRestClient client = cluster.getRestClient(NOT_EXISTING_USER, TEST_USER.getPassword())) { - HttpResponse response = client.getAuthInfo(); - - assertThat(response, is(notNullValue())); - response.assertStatusCode(SC_UNAUTHORIZED); - } - } - - @Test - public void shouldRespondWith401WhenPasswordIsIncorrect() { - try (TestRestClient client = cluster.getRestClient(TEST_USER.getName(), INVALID_PASSWORD)) { - HttpResponse response = client.getAuthInfo(); - - assertThat(response, is(notNullValue())); - response.assertStatusCode(SC_UNAUTHORIZED); - } - } - - @Test - public void shouldRespondWith200WhenCredentialsAreCorrect() { - try (TestRestClient client = cluster.getRestClient(TEST_USER)) { - - HttpResponse response = client.getAuthInfo(); - - assertThat(response, is(notNullValue())); - response.assertStatusCode(SC_OK); - } - } - - @Test - public void testBrowserShouldRequestForCredentials() { - try (TestRestClient client = cluster.getRestClient()) { - - HttpResponse response = client.getAuthInfo(); - - assertThat(response, is(notNullValue())); - response.assertStatusCode(SC_UNAUTHORIZED); - assertThatBrowserAskUserForCredentials(response); - } - } - - @Test - public void testUserShouldNotHaveAssignedCustomAttributes() { - try (TestRestClient client = cluster.getRestClient(TEST_USER)) { - - HttpResponse response = client.getAuthInfo(); - - assertThat(response, is(notNullValue())); - response.assertStatusCode(SC_OK); - AuthInfo authInfo = response.getBodyAs(AuthInfo.class); - assertThat(authInfo, is(notNullValue())); - assertThat(authInfo.getCustomAttributeNames(), is(notNullValue())); - assertThat(authInfo.getCustomAttributeNames(), hasSize(0)); - } - } - - @Test - public void testUserShouldHaveAssignedCustomAttributes() { - try (TestRestClient client = cluster.getRestClient(SUPER_USER)) { - - HttpResponse response = client.getAuthInfo(); - - assertThat(response, is(notNullValue())); - response.assertStatusCode(SC_OK); - AuthInfo authInfo = response.getBodyAs(AuthInfo.class); - assertThat(authInfo, is(notNullValue())); - List customAttributeNames = authInfo.getCustomAttributeNames(); - assertThat(customAttributeNames, is(notNullValue())); - assertThat(customAttributeNames, hasSize(1)); - assertThat(customAttributeNames.get(0), Matchers.equalTo("attr.internal." + CUSTOM_ATTRIBUTE_NAME)); - } - } - - private void assertThatBrowserAskUserForCredentials(HttpResponse response) { - String reason = "Browser does not ask user for credentials"; - assertThat(reason, response.containHeader(HttpHeaders.WWW_AUTHENTICATE), equalTo(true)); - assertThat(response.getHeader(HttpHeaders.WWW_AUTHENTICATE).getValue(), containsStringIgnoringCase("basic")); - } + static final User TEST_USER = new User("test_user").password("s3cret"); + + public static final String CUSTOM_ATTRIBUTE_NAME = "superhero"; + static final User SUPER_USER = new User("super-user").password("super-password").attr(CUSTOM_ATTRIBUTE_NAME, true); + public static final String NOT_EXISTING_USER = "not-existing-user"; + public static final String INVALID_PASSWORD = "secret-password"; + + public static final AuthcDomain AUTHC_DOMAIN = new AuthcDomain("basic", 0).httpAuthenticatorWithChallenge("basic").backend("internal"); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authc(AUTHC_DOMAIN) + .users(TEST_USER, SUPER_USER) + .build(); + + @Test + public void shouldRespondWith401WhenUserDoesNotExist() { + try (TestRestClient client = cluster.getRestClient(NOT_EXISTING_USER, INVALID_PASSWORD)) { + HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + + @Test + public void shouldRespondWith401WhenUserNameIsIncorrect() { + try (TestRestClient client = cluster.getRestClient(NOT_EXISTING_USER, TEST_USER.getPassword())) { + HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + + @Test + public void shouldRespondWith401WhenPasswordIsIncorrect() { + try (TestRestClient client = cluster.getRestClient(TEST_USER.getName(), INVALID_PASSWORD)) { + HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + response.assertStatusCode(SC_UNAUTHORIZED); + } + } + + @Test + public void shouldRespondWith200WhenCredentialsAreCorrect() { + try (TestRestClient client = cluster.getRestClient(TEST_USER)) { + + HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + response.assertStatusCode(SC_OK); + } + } + + @Test + public void testBrowserShouldRequestForCredentials() { + try (TestRestClient client = cluster.getRestClient()) { + + HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + response.assertStatusCode(SC_UNAUTHORIZED); + assertThatBrowserAskUserForCredentials(response); + } + } + + @Test + public void testUserShouldNotHaveAssignedCustomAttributes() { + try (TestRestClient client = cluster.getRestClient(TEST_USER)) { + + HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + response.assertStatusCode(SC_OK); + AuthInfo authInfo = response.getBodyAs(AuthInfo.class); + assertThat(authInfo, is(notNullValue())); + assertThat(authInfo.getCustomAttributeNames(), is(notNullValue())); + assertThat(authInfo.getCustomAttributeNames(), hasSize(0)); + } + } + + @Test + public void testUserShouldHaveAssignedCustomAttributes() { + try (TestRestClient client = cluster.getRestClient(SUPER_USER)) { + + HttpResponse response = client.getAuthInfo(); + + assertThat(response, is(notNullValue())); + response.assertStatusCode(SC_OK); + AuthInfo authInfo = response.getBodyAs(AuthInfo.class); + assertThat(authInfo, is(notNullValue())); + List customAttributeNames = authInfo.getCustomAttributeNames(); + assertThat(customAttributeNames, is(notNullValue())); + assertThat(customAttributeNames, hasSize(1)); + assertThat(customAttributeNames.get(0), Matchers.equalTo("attr.internal." + CUSTOM_ATTRIBUTE_NAME)); + } + } + + private void assertThatBrowserAskUserForCredentials(HttpResponse response) { + String reason = "Browser does not ask user for credentials"; + assertThat(reason, response.containHeader(HttpHeaders.WWW_AUTHENTICATE), equalTo(true)); + assertThat(response.getHeader(HttpHeaders.WWW_AUTHENTICATE).getValue(), containsStringIgnoringCase("basic")); + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/BasicAuthWithoutChallengeTests.java b/src/integrationTest/java/org/opensearch/security/http/BasicAuthWithoutChallengeTests.java index 4af5563e53..940f326cc1 100644 --- a/src/integrationTest/java/org/opensearch/security/http/BasicAuthWithoutChallengeTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/BasicAuthWithoutChallengeTests.java @@ -28,24 +28,25 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class BasicAuthWithoutChallengeTests { - @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .authc(AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE).build(); - - @Test - public void browserShouldNotRequestUserForCredentials() { - try (TestRestClient client = cluster.getRestClient()) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - assertThatBrowserDoesNotAskUserForCredentials(response); - } - } - - private void assertThatBrowserDoesNotAskUserForCredentials(HttpResponse response) { - String reason = "Browser asked user for credentials which is not expected"; - assertThat(reason, response.containHeader(HttpHeaders.WWW_AUTHENTICATE), equalTo(false)); - } + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authc(AUTHC_HTTPBASIC_INTERNAL_WITHOUT_CHALLENGE) + .build(); + + @Test + public void browserShouldNotRequestUserForCredentials() { + try (TestRestClient client = cluster.getRestClient()) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + assertThatBrowserDoesNotAskUserForCredentials(response); + } + } + + private void assertThatBrowserDoesNotAskUserForCredentials(HttpResponse response) { + String reason = "Browser asked user for credentials which is not expected"; + assertThat(reason, response.containHeader(HttpHeaders.WWW_AUTHENTICATE), equalTo(false)); + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java index 18a79abcef..144a54c4d6 100644 --- a/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/CertificateAuthenticationTest.java @@ -40,106 +40,109 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class CertificateAuthenticationTest { - private static final User USER_ADMIN = new User("admin").roles(ALL_ACCESS); - - public static final String POINTER_BACKEND_ROLES = "/backend_roles"; - public static final String POINTER_ROLES = "/roles"; - - private static final String USER_SPOCK = "spock"; - private static final String USER_KIRK = "kirk"; - - private static final String BACKEND_ROLE_BRIDGE = "bridge"; - private static final String BACKEND_ROLE_CAPTAIN = "captain"; - - private static final Role ROLE_ALL_INDEX_SEARCH = new Role("all-index-search").indexPermissions("indices:data/read/search") - .on("*"); - - private static final Map CERT_AUTH_CONFIG = Map.of( - "username_attribute", "cn", - "roles_attribute", "ou" - ); - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .nodeSettings(Map.of("plugins.security.ssl.http.clientauth_mode", "OPTIONAL")) - .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).anonymousAuth(false) - .authc(new AuthcDomain("clientcert_auth_domain", -1, true) - .httpAuthenticator(new HttpAuthenticator("clientcert").challenge(false) - .config(CERT_AUTH_CONFIG)).backend("noop")) - .authc(AUTHC_HTTPBASIC_INTERNAL).roles(ROLE_ALL_INDEX_SEARCH).users(USER_ADMIN) - .rolesMapping(new RolesMapping(ROLE_ALL_INDEX_SEARCH).backendRoles(BACKEND_ROLE_BRIDGE)).build(); - - private static final TestCertificates TEST_CERTIFICATES = cluster.getTestCertificates(); - - @Test - public void shouldAuthenticateUserWithBasicAuthWhenCertificateAuthenticationIsConfigured() { - try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_OK); - } - } - - @Test - public void shouldAuthenticateUserWithCertificate_positiveUserSpoke() { - CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_BRIDGE, USER_SPOCK); - try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) { - - client.assertCorrectCredentials(USER_SPOCK); - } - } - - @Test - public void shouldAuthenticateUserWithCertificate_positiveUserKirk() { - CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_BRIDGE, USER_KIRK); - try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) { - - client.assertCorrectCredentials(USER_KIRK); - } - } - - @Test - public void shouldAuthenticateUserWithCertificate_negative() { - CertificateData untrustedUserCertificate = TEST_CERTIFICATES.createSelfSignedCertificate("CN=untrusted"); - try (TestRestClient client = cluster.getRestClient(untrustedUserCertificate)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - } - } - - @Test - public void shouldRetrieveBackendRoleFromCertificate_positiveRoleBridge() { - CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_BRIDGE, USER_KIRK); - try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(backendRoles, hasSize(1)); - assertThat(backendRoles, containsInAnyOrder(BACKEND_ROLE_BRIDGE)); - List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); - assertThat(roles, hasSize(1)); - assertThat(roles, containsInAnyOrder(ROLE_ALL_INDEX_SEARCH.getName())); - } - } - - @Test - public void shouldRetrieveBackendRoleFromCertificate_positiveRoleCaptain() { - CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_CAPTAIN, USER_KIRK); - try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(backendRoles, hasSize(1)); - assertThat(backendRoles, containsInAnyOrder(BACKEND_ROLE_CAPTAIN)); - List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); - assertThat(roles, hasSize(0)); - } - } + private static final User USER_ADMIN = new User("admin").roles(ALL_ACCESS); + + public static final String POINTER_BACKEND_ROLES = "/backend_roles"; + public static final String POINTER_ROLES = "/roles"; + + private static final String USER_SPOCK = "spock"; + private static final String USER_KIRK = "kirk"; + + private static final String BACKEND_ROLE_BRIDGE = "bridge"; + private static final String BACKEND_ROLE_CAPTAIN = "captain"; + + private static final Role ROLE_ALL_INDEX_SEARCH = new Role("all-index-search").indexPermissions("indices:data/read/search").on("*"); + + private static final Map CERT_AUTH_CONFIG = Map.of("username_attribute", "cn", "roles_attribute", "ou"); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().nodeSettings( + Map.of("plugins.security.ssl.http.clientauth_mode", "OPTIONAL") + ) + .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .anonymousAuth(false) + .authc( + new AuthcDomain("clientcert_auth_domain", -1, true).httpAuthenticator( + new HttpAuthenticator("clientcert").challenge(false).config(CERT_AUTH_CONFIG) + ).backend("noop") + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .roles(ROLE_ALL_INDEX_SEARCH) + .users(USER_ADMIN) + .rolesMapping(new RolesMapping(ROLE_ALL_INDEX_SEARCH).backendRoles(BACKEND_ROLE_BRIDGE)) + .build(); + + private static final TestCertificates TEST_CERTIFICATES = cluster.getTestCertificates(); + + @Test + public void shouldAuthenticateUserWithBasicAuthWhenCertificateAuthenticationIsConfigured() { + try (TestRestClient client = cluster.getRestClient(USER_ADMIN)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_OK); + } + } + + @Test + public void shouldAuthenticateUserWithCertificate_positiveUserSpoke() { + CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_BRIDGE, USER_SPOCK); + try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) { + + client.assertCorrectCredentials(USER_SPOCK); + } + } + + @Test + public void shouldAuthenticateUserWithCertificate_positiveUserKirk() { + CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_BRIDGE, USER_KIRK); + try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) { + + client.assertCorrectCredentials(USER_KIRK); + } + } + + @Test + public void shouldAuthenticateUserWithCertificate_negative() { + CertificateData untrustedUserCertificate = TEST_CERTIFICATES.createSelfSignedCertificate("CN=untrusted"); + try (TestRestClient client = cluster.getRestClient(untrustedUserCertificate)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + } + } + + @Test + public void shouldRetrieveBackendRoleFromCertificate_positiveRoleBridge() { + CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_BRIDGE, USER_KIRK); + try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(1)); + assertThat(backendRoles, containsInAnyOrder(BACKEND_ROLE_BRIDGE)); + List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); + assertThat(roles, hasSize(1)); + assertThat(roles, containsInAnyOrder(ROLE_ALL_INDEX_SEARCH.getName())); + } + } + + @Test + public void shouldRetrieveBackendRoleFromCertificate_positiveRoleCaptain() { + CertificateData userSpockCertificate = TEST_CERTIFICATES.issueUserCertificate(BACKEND_ROLE_CAPTAIN, USER_KIRK); + try (TestRestClient client = cluster.getRestClient(userSpockCertificate)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(1)); + assertThat(backendRoles, containsInAnyOrder(BACKEND_ROLE_CAPTAIN)); + List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); + assertThat(roles, hasSize(0)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java b/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java index c17ccb8ea3..49ded4f2a9 100644 --- a/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/CommonProxyAuthenticationTests.java @@ -31,228 +31,225 @@ */ abstract class CommonProxyAuthenticationTests { - protected static final String RESOURCE_AUTH_INFO = "/_opendistro/_security/authinfo"; - protected static final TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - - protected static final String ATTRIBUTE_DEPARTMENT = "department"; - protected static final String ATTRIBUTE_SKILLS = "skills"; - - protected static final String USER_ATTRIBUTE_DEPARTMENT_NAME = "attr.proxy." + ATTRIBUTE_DEPARTMENT; - protected static final String USER_ATTRIBUTE_SKILLS_NAME = "attr.proxy." + ATTRIBUTE_SKILLS; - protected static final String USER_ATTRIBUTE_USERNAME_NAME = "attr.proxy.username"; - - protected static final String HEADER_PREFIX_CUSTOM_ATTRIBUTES = "x-custom-attr"; - protected static final String HEADER_PROXY_USER = "x-proxy-user"; - protected static final String HEADER_PROXY_ROLES = "x-proxy-roles"; - protected static final String HEADER_FORWARDED_FOR = "X-Forwarded-For"; - protected static final String HEADER_DEPARTMENT = HEADER_PREFIX_CUSTOM_ATTRIBUTES + ATTRIBUTE_DEPARTMENT; - protected static final String HEADER_SKILLS = HEADER_PREFIX_CUSTOM_ATTRIBUTES + ATTRIBUTE_SKILLS; - - protected static final String IP_PROXY = "127.0.0.10"; - protected static final String IP_NON_PROXY = "127.0.0.5"; - protected static final String IP_CLIENT = "127.0.0.1"; - - protected static final String USER_KIRK = "kirk"; - protected static final String USER_SPOCK = "spock"; - - protected static final String BACKEND_ROLE_FIRST_MATE = "firstMate"; - protected static final String BACKEND_ROLE_CAPTAIN = "captain"; - protected static final String DEPARTMENT_BRIDGE = "bridge"; - - protected static final String PERSONAL_INDEX_NAME_PATTERN = "personal-${" + USER_ATTRIBUTE_DEPARTMENT_NAME + "}-${" + USER_ATTRIBUTE_USERNAME_NAME + "}"; - protected static final String PERSONAL_INDEX_NAME_SPOCK = "personal-" + DEPARTMENT_BRIDGE + "-" + USER_SPOCK; - protected static final String PERSONAL_INDEX_NAME_KIRK = "personal-" + DEPARTMENT_BRIDGE + "-" + USER_KIRK; - - protected static final String POINTER_USERNAME = "/user_name"; - protected static final String POINTER_BACKEND_ROLES = "/backend_roles"; - protected static final String POINTER_ROLES = "/roles"; - protected static final String POINTER_CUSTOM_ATTRIBUTES = "/custom_attribute_names"; - protected static final String POINTER_TOTAL_HITS = "/hits/total/value"; - protected static final String POINTER_FIRST_DOCUMENT_ID = "/hits/hits/0/_id"; - protected static final String POINTER_FIRST_DOCUMENT_INDEX = "/hits/hits/0/_index"; - protected static final String POINTER_FIRST_DOCUMENT_SOURCE_TITLE = "/hits/hits/0/_source/title"; - - protected static final TestSecurityConfig.Role ROLE_ALL_INDEX_SEARCH = new TestSecurityConfig.Role("all-index-search").indexPermissions("indices:data/read/search") - .on("*"); - - protected static final TestSecurityConfig.Role ROLE_PERSONAL_INDEX_SEARCH = new TestSecurityConfig.Role("personal-index-search").indexPermissions("indices:data/read/search") - .on(PERSONAL_INDEX_NAME_PATTERN); - - protected static final RolesMapping ROLES_MAPPING_CAPTAIN = new RolesMapping(ROLE_PERSONAL_INDEX_SEARCH).backendRoles(BACKEND_ROLE_CAPTAIN); - - protected static final RolesMapping ROLES_MAPPING_FIRST_MATE = new RolesMapping(ROLE_ALL_INDEX_SEARCH) - .backendRoles(BACKEND_ROLE_FIRST_MATE); - - protected abstract LocalCluster getCluster(); - - protected void shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured() { - try(TestRestClient client = getCluster().getRestClient(USER_ADMIN)) { - TestRestClient.HttpResponse response = client.get(RESOURCE_AUTH_INFO); - - response.assertStatusCode(200); - } - } - - protected void shouldAuthenticateWithProxy_positiveUserKirk() throws IOException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_USER, USER_KIRK) - .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN); - try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(USER_KIRK)); - } - } - - protected void shouldAuthenticateWithProxy_positiveUserSpock() throws IOException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_USER, USER_SPOCK) - .header(HEADER_PROXY_ROLES, BACKEND_ROLE_FIRST_MATE); - try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(USER_SPOCK)); - } - } - - protected void shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing() throws IOException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_PROXY_USER, USER_KIRK) - .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN); - try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - } - } - - protected void shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing() throws IOException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN); - try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - } - } - - protected void shouldAuthenticateWithProxyWhenRolesHeaderIsMissing() throws IOException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_USER, USER_KIRK); - try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(USER_KIRK)); - } - } - - protected void shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy() throws IOException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_NON_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_USER, USER_KIRK); - try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - } - } - - protected void shouldRetrieveEmptyListOfRoles() throws IOException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_USER, USER_SPOCK); - try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(backendRoles, hasSize(0)); - List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); - assertThat(roles, hasSize(0)); - } - } - - protected void shouldRetrieveSingleRoleFirstMate() throws IOException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_USER, USER_SPOCK) - .header(HEADER_PROXY_ROLES, BACKEND_ROLE_FIRST_MATE); - try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(backendRoles, hasSize(1)); - assertThat(backendRoles, contains(BACKEND_ROLE_FIRST_MATE)); - List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); - assertThat(roles, hasSize(1)); - assertThat(roles, contains(ROLE_ALL_INDEX_SEARCH.getName())); - } - } - - protected void shouldRetrieveSingleRoleCaptain() throws IOException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_USER, USER_SPOCK) - .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN); - try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(backendRoles, hasSize(1)); - assertThat(backendRoles, contains(BACKEND_ROLE_CAPTAIN)); - List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); - assertThat(roles, hasSize(1)); - assertThat(roles, contains(ROLE_PERSONAL_INDEX_SEARCH.getName())); - } - } - - protected void shouldRetrieveMultipleRoles() throws IOException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_USER, USER_SPOCK) - .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN + "," + BACKEND_ROLE_FIRST_MATE); - try(TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { - - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(backendRoles, hasSize(2)); - assertThat(backendRoles, containsInAnyOrder(BACKEND_ROLE_CAPTAIN, BACKEND_ROLE_FIRST_MATE)); - List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); - assertThat(roles, hasSize(2)); - assertThat(roles, containsInAnyOrder(ROLE_PERSONAL_INDEX_SEARCH.getName(), ROLE_ALL_INDEX_SEARCH.getName())); - } - } + protected static final String RESOURCE_AUTH_INFO = "/_opendistro/_security/authinfo"; + protected static final TestSecurityConfig.User USER_ADMIN = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + protected static final String ATTRIBUTE_DEPARTMENT = "department"; + protected static final String ATTRIBUTE_SKILLS = "skills"; + + protected static final String USER_ATTRIBUTE_DEPARTMENT_NAME = "attr.proxy." + ATTRIBUTE_DEPARTMENT; + protected static final String USER_ATTRIBUTE_SKILLS_NAME = "attr.proxy." + ATTRIBUTE_SKILLS; + protected static final String USER_ATTRIBUTE_USERNAME_NAME = "attr.proxy.username"; + + protected static final String HEADER_PREFIX_CUSTOM_ATTRIBUTES = "x-custom-attr"; + protected static final String HEADER_PROXY_USER = "x-proxy-user"; + protected static final String HEADER_PROXY_ROLES = "x-proxy-roles"; + protected static final String HEADER_FORWARDED_FOR = "X-Forwarded-For"; + protected static final String HEADER_DEPARTMENT = HEADER_PREFIX_CUSTOM_ATTRIBUTES + ATTRIBUTE_DEPARTMENT; + protected static final String HEADER_SKILLS = HEADER_PREFIX_CUSTOM_ATTRIBUTES + ATTRIBUTE_SKILLS; + + protected static final String IP_PROXY = "127.0.0.10"; + protected static final String IP_NON_PROXY = "127.0.0.5"; + protected static final String IP_CLIENT = "127.0.0.1"; + + protected static final String USER_KIRK = "kirk"; + protected static final String USER_SPOCK = "spock"; + + protected static final String BACKEND_ROLE_FIRST_MATE = "firstMate"; + protected static final String BACKEND_ROLE_CAPTAIN = "captain"; + protected static final String DEPARTMENT_BRIDGE = "bridge"; + + protected static final String PERSONAL_INDEX_NAME_PATTERN = "personal-${" + + USER_ATTRIBUTE_DEPARTMENT_NAME + + "}-${" + + USER_ATTRIBUTE_USERNAME_NAME + + "}"; + protected static final String PERSONAL_INDEX_NAME_SPOCK = "personal-" + DEPARTMENT_BRIDGE + "-" + USER_SPOCK; + protected static final String PERSONAL_INDEX_NAME_KIRK = "personal-" + DEPARTMENT_BRIDGE + "-" + USER_KIRK; + + protected static final String POINTER_USERNAME = "/user_name"; + protected static final String POINTER_BACKEND_ROLES = "/backend_roles"; + protected static final String POINTER_ROLES = "/roles"; + protected static final String POINTER_CUSTOM_ATTRIBUTES = "/custom_attribute_names"; + protected static final String POINTER_TOTAL_HITS = "/hits/total/value"; + protected static final String POINTER_FIRST_DOCUMENT_ID = "/hits/hits/0/_id"; + protected static final String POINTER_FIRST_DOCUMENT_INDEX = "/hits/hits/0/_index"; + protected static final String POINTER_FIRST_DOCUMENT_SOURCE_TITLE = "/hits/hits/0/_source/title"; + + protected static final TestSecurityConfig.Role ROLE_ALL_INDEX_SEARCH = new TestSecurityConfig.Role("all-index-search").indexPermissions( + "indices:data/read/search" + ).on("*"); + + protected static final TestSecurityConfig.Role ROLE_PERSONAL_INDEX_SEARCH = new TestSecurityConfig.Role("personal-index-search") + .indexPermissions("indices:data/read/search") + .on(PERSONAL_INDEX_NAME_PATTERN); + + protected static final RolesMapping ROLES_MAPPING_CAPTAIN = new RolesMapping(ROLE_PERSONAL_INDEX_SEARCH).backendRoles( + BACKEND_ROLE_CAPTAIN + ); + + protected static final RolesMapping ROLES_MAPPING_FIRST_MATE = new RolesMapping(ROLE_ALL_INDEX_SEARCH).backendRoles( + BACKEND_ROLE_FIRST_MATE + ); + + protected abstract LocalCluster getCluster(); + + protected void shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured() { + try (TestRestClient client = getCluster().getRestClient(USER_ADMIN)) { + TestRestClient.HttpResponse response = client.get(RESOURCE_AUTH_INFO); + + response.assertStatusCode(200); + } + } + + protected void shouldAuthenticateWithProxy_positiveUserKirk() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ).header(HEADER_FORWARDED_FOR, IP_CLIENT).header(HEADER_PROXY_USER, USER_KIRK).header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN); + try (TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_KIRK)); + } + } + + protected void shouldAuthenticateWithProxy_positiveUserSpock() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ).header(HEADER_FORWARDED_FOR, IP_CLIENT).header(HEADER_PROXY_USER, USER_SPOCK).header(HEADER_PROXY_ROLES, BACKEND_ROLE_FIRST_MATE); + try (TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_SPOCK)); + } + } + + protected void shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ).header(HEADER_PROXY_USER, USER_KIRK).header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN); + try (TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + } + } + + protected void shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ).header(HEADER_FORWARDED_FOR, IP_CLIENT).header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN); + try (TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + } + } + + protected void shouldAuthenticateWithProxyWhenRolesHeaderIsMissing() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ).header(HEADER_FORWARDED_FOR, IP_CLIENT).header(HEADER_PROXY_USER, USER_KIRK); + try (TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_KIRK)); + } + } + + protected void shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_NON_PROXY) + ).header(HEADER_FORWARDED_FOR, IP_CLIENT).header(HEADER_PROXY_USER, USER_KIRK); + try (TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + } + } + + protected void shouldRetrieveEmptyListOfRoles() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ).header(HEADER_FORWARDED_FOR, IP_CLIENT).header(HEADER_PROXY_USER, USER_SPOCK); + try (TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(0)); + List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); + assertThat(roles, hasSize(0)); + } + } + + protected void shouldRetrieveSingleRoleFirstMate() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ).header(HEADER_FORWARDED_FOR, IP_CLIENT).header(HEADER_PROXY_USER, USER_SPOCK).header(HEADER_PROXY_ROLES, BACKEND_ROLE_FIRST_MATE); + try (TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(1)); + assertThat(backendRoles, contains(BACKEND_ROLE_FIRST_MATE)); + List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); + assertThat(roles, hasSize(1)); + assertThat(roles, contains(ROLE_ALL_INDEX_SEARCH.getName())); + } + } + + protected void shouldRetrieveSingleRoleCaptain() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ).header(HEADER_FORWARDED_FOR, IP_CLIENT).header(HEADER_PROXY_USER, USER_SPOCK).header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN); + try (TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(1)); + assertThat(backendRoles, contains(BACKEND_ROLE_CAPTAIN)); + List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); + assertThat(roles, hasSize(1)); + assertThat(roles, contains(ROLE_PERSONAL_INDEX_SEARCH.getName())); + } + } + + protected void shouldRetrieveMultipleRoles() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN + "," + BACKEND_ROLE_FIRST_MATE); + try (TestRestClient client = getCluster().createGenericClientRestClient(testRestClientConfiguration)) { + + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(2)); + assertThat(backendRoles, containsInAnyOrder(BACKEND_ROLE_CAPTAIN, BACKEND_ROLE_FIRST_MATE)); + List roles = response.getTextArrayFromJsonBody(POINTER_ROLES); + assertThat(roles, hasSize(2)); + assertThat(roles, containsInAnyOrder(ROLE_PERSONAL_INDEX_SEARCH.getName(), ROLE_ALL_INDEX_SEARCH.getName())); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/DirectoryInformationTrees.java b/src/integrationTest/java/org/opensearch/security/http/DirectoryInformationTrees.java index 8403106522..3f9c220923 100644 --- a/src/integrationTest/java/org/opensearch/security/http/DirectoryInformationTrees.java +++ b/src/integrationTest/java/org/opensearch/security/http/DirectoryInformationTrees.java @@ -14,111 +14,110 @@ class DirectoryInformationTrees { - public static final String DN_PEOPLE_TEST_ORG = "ou=people,o=test.org"; - public static final String DN_OPEN_SEARCH_PEOPLE_TEST_ORG = "cn=Open Search,ou=people,o=test.org"; - public static final String DN_CHRISTPHER_PEOPLE_TEST_ORG = "cn=Christpher,ou=people,o=test.org"; - public static final String DN_KIRK_PEOPLE_TEST_ORG = "cn=Kirk,ou=people,o=test.org"; - public static final String DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG = "cn=Captain Spock,ou=people,o=test.org"; - public static final String DN_LEONARD_PEOPLE_TEST_ORG = "cn=Leonard,ou=people,o=test.org"; - public static final String DN_JEAN_PEOPLE_TEST_ORG = "cn=Jean,ou=people,o=test.org"; - public static final String DN_GROUPS_TEST_ORG = "ou=groups,o=test.org"; - public static final String DN_BRIDGE_GROUPS_TEST_ORG = "cn=bridge,ou=groups,o=test.org"; + public static final String DN_PEOPLE_TEST_ORG = "ou=people,o=test.org"; + public static final String DN_OPEN_SEARCH_PEOPLE_TEST_ORG = "cn=Open Search,ou=people,o=test.org"; + public static final String DN_CHRISTPHER_PEOPLE_TEST_ORG = "cn=Christpher,ou=people,o=test.org"; + public static final String DN_KIRK_PEOPLE_TEST_ORG = "cn=Kirk,ou=people,o=test.org"; + public static final String DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG = "cn=Captain Spock,ou=people,o=test.org"; + public static final String DN_LEONARD_PEOPLE_TEST_ORG = "cn=Leonard,ou=people,o=test.org"; + public static final String DN_JEAN_PEOPLE_TEST_ORG = "cn=Jean,ou=people,o=test.org"; + public static final String DN_GROUPS_TEST_ORG = "ou=groups,o=test.org"; + public static final String DN_BRIDGE_GROUPS_TEST_ORG = "cn=bridge,ou=groups,o=test.org"; - public static final String USER_KIRK = "kirk"; - public static final String PASSWORD_KIRK = "kirk-secret"; - public static final String USER_SPOCK = "spock"; - public static final String PASSWORD_SPOCK = "spocksecret"; - public static final String USER_OPENS = "opens"; - public static final String PASSWORD_OPEN_SEARCH = "open_search-secret"; - public static final String USER_JEAN = "jean"; - public static final String PASSWORD_JEAN = "jeansecret"; - public static final String USER_LEONARD = "leonard"; - public static final String PASSWORD_LEONARD = "Leonard-secret"; - public static final String PASSWORD_CHRISTPHER = "christpher_secret"; + public static final String USER_KIRK = "kirk"; + public static final String PASSWORD_KIRK = "kirk-secret"; + public static final String USER_SPOCK = "spock"; + public static final String PASSWORD_SPOCK = "spocksecret"; + public static final String USER_OPENS = "opens"; + public static final String PASSWORD_OPEN_SEARCH = "open_search-secret"; + public static final String USER_JEAN = "jean"; + public static final String PASSWORD_JEAN = "jeansecret"; + public static final String USER_LEONARD = "leonard"; + public static final String PASSWORD_LEONARD = "Leonard-secret"; + public static final String PASSWORD_CHRISTPHER = "christpher_secret"; - public static final String CN_GROUP_ADMIN = "admin"; - public static final String CN_GROUP_CREW = "crew"; - public static final String CN_GROUP_BRIDGE = "bridge"; + public static final String CN_GROUP_ADMIN = "admin"; + public static final String CN_GROUP_CREW = "crew"; + public static final String CN_GROUP_BRIDGE = "bridge"; - public static final String USER_SEARCH = "(uid={0})"; - public static final String USERNAME_ATTRIBUTE = "uid"; + public static final String USER_SEARCH = "(uid={0})"; + public static final String USERNAME_ATTRIBUTE = "uid"; - static final LdifData LDIF_DATA = new LdifBuilder() - .root("o=test.org") - .dc("TEST") - .classes("top", "domain") - .newRecord(DN_PEOPLE_TEST_ORG) - .ou("people") - .classes("organizationalUnit", "top") - .newRecord(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) - .classes("inetOrgPerson") - .cn("Open Search") - .sn("Search") - .uid(USER_OPENS) - .userPassword(PASSWORD_OPEN_SEARCH) - .mail("open.search@example.com") - .ou("Human Resources") - .newRecord(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) - .classes("inetOrgPerson") - .cn("Captain Spock") - .sn(USER_SPOCK) - .uid(USER_SPOCK) - .userPassword(PASSWORD_SPOCK) - .mail("spock@example.com") - .ou("Human Resources") - .newRecord(DN_KIRK_PEOPLE_TEST_ORG) - .classes("inetOrgPerson") - .cn("Kirk") - .sn("Kirk") - .uid(USER_KIRK) - .userPassword(PASSWORD_KIRK) - .mail("spock@example.com") - .ou("Human Resources") - .newRecord(DN_CHRISTPHER_PEOPLE_TEST_ORG) - .classes("inetOrgPerson") - .cn("Christpher") - .sn("Christpher") - .uid("christpher") - .userPassword(PASSWORD_CHRISTPHER) - .mail("christpher@example.com") - .ou("Human Resources") - .newRecord(DN_LEONARD_PEOPLE_TEST_ORG) - .classes("inetOrgPerson") - .cn("Leonard") - .sn("Leonard") - .uid(USER_LEONARD) - .userPassword(PASSWORD_LEONARD) - .mail("leonard@example.com") - .ou("Human Resources") - .newRecord(DN_JEAN_PEOPLE_TEST_ORG) - .classes("inetOrgPerson") - .cn("Jean") - .sn("Jean") - .uid(USER_JEAN) - .userPassword(PASSWORD_JEAN) - .mail("jean@example.com") - .ou("Human Resources") - .newRecord(DN_GROUPS_TEST_ORG) - .ou("groups") - .cn("groupsRoot") - .classes("groupofuniquenames", "top") - .newRecord("cn=admin,ou=groups,o=test.org") - .ou("groups") - .cn(CN_GROUP_ADMIN) - .uniqueMember(DN_KIRK_PEOPLE_TEST_ORG) - .classes("groupofuniquenames", "top") - .newRecord("cn=crew,ou=groups,o=test.org") - .ou("groups") - .cn(CN_GROUP_CREW) - .uniqueMember(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) - .uniqueMember(DN_CHRISTPHER_PEOPLE_TEST_ORG) - .uniqueMember(DN_BRIDGE_GROUPS_TEST_ORG) - .classes("groupofuniquenames", "top") - .newRecord(DN_BRIDGE_GROUPS_TEST_ORG) - .ou("groups") - .cn(CN_GROUP_BRIDGE) - .uniqueMember(DN_JEAN_PEOPLE_TEST_ORG) - .classes("groupofuniquenames", "top") - .buildRecord() - .buildLdif(); + static final LdifData LDIF_DATA = new LdifBuilder().root("o=test.org") + .dc("TEST") + .classes("top", "domain") + .newRecord(DN_PEOPLE_TEST_ORG) + .ou("people") + .classes("organizationalUnit", "top") + .newRecord(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Open Search") + .sn("Search") + .uid(USER_OPENS) + .userPassword(PASSWORD_OPEN_SEARCH) + .mail("open.search@example.com") + .ou("Human Resources") + .newRecord(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Captain Spock") + .sn(USER_SPOCK) + .uid(USER_SPOCK) + .userPassword(PASSWORD_SPOCK) + .mail("spock@example.com") + .ou("Human Resources") + .newRecord(DN_KIRK_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Kirk") + .sn("Kirk") + .uid(USER_KIRK) + .userPassword(PASSWORD_KIRK) + .mail("spock@example.com") + .ou("Human Resources") + .newRecord(DN_CHRISTPHER_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Christpher") + .sn("Christpher") + .uid("christpher") + .userPassword(PASSWORD_CHRISTPHER) + .mail("christpher@example.com") + .ou("Human Resources") + .newRecord(DN_LEONARD_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Leonard") + .sn("Leonard") + .uid(USER_LEONARD) + .userPassword(PASSWORD_LEONARD) + .mail("leonard@example.com") + .ou("Human Resources") + .newRecord(DN_JEAN_PEOPLE_TEST_ORG) + .classes("inetOrgPerson") + .cn("Jean") + .sn("Jean") + .uid(USER_JEAN) + .userPassword(PASSWORD_JEAN) + .mail("jean@example.com") + .ou("Human Resources") + .newRecord(DN_GROUPS_TEST_ORG) + .ou("groups") + .cn("groupsRoot") + .classes("groupofuniquenames", "top") + .newRecord("cn=admin,ou=groups,o=test.org") + .ou("groups") + .cn(CN_GROUP_ADMIN) + .uniqueMember(DN_KIRK_PEOPLE_TEST_ORG) + .classes("groupofuniquenames", "top") + .newRecord("cn=crew,ou=groups,o=test.org") + .ou("groups") + .cn(CN_GROUP_CREW) + .uniqueMember(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) + .uniqueMember(DN_CHRISTPHER_PEOPLE_TEST_ORG) + .uniqueMember(DN_BRIDGE_GROUPS_TEST_ORG) + .classes("groupofuniquenames", "top") + .newRecord(DN_BRIDGE_GROUPS_TEST_ORG) + .ou("groups") + .cn(CN_GROUP_BRIDGE) + .uniqueMember(DN_JEAN_PEOPLE_TEST_ORG) + .classes("groupofuniquenames", "top") + .buildRecord() + .buildLdif(); } diff --git a/src/integrationTest/java/org/opensearch/security/http/DisabledBasicAuthTests.java b/src/integrationTest/java/org/opensearch/security/http/DisabledBasicAuthTests.java index 5398ea77f7..7ae856ba8b 100644 --- a/src/integrationTest/java/org/opensearch/security/http/DisabledBasicAuthTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/DisabledBasicAuthTests.java @@ -28,20 +28,21 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class DisabledBasicAuthTests { - @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .authc(DISABLED_AUTHC_HTTPBASIC_INTERNAL).users(TEST_USER) - .authc(JWT_AUTH_DOMAIN) - .build(); - - @Test - public void shouldRespondWith401WhenCredentialsAreCorrectButBasicAuthIsDisabled() { - try (TestRestClient client = cluster.getRestClient(TEST_USER)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(SC_UNAUTHORIZED); - } - } + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authc(DISABLED_AUTHC_HTTPBASIC_INTERNAL) + .users(TEST_USER) + .authc(JWT_AUTH_DOMAIN) + .build(); + + @Test + public void shouldRespondWith401WhenCredentialsAreCorrectButBasicAuthIsDisabled() { + try (TestRestClient client = cluster.getRestClient(TEST_USER)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(SC_UNAUTHORIZED); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/ExtendedProxyAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/ExtendedProxyAuthenticationTest.java index 4278a07a53..6fcc7eac83 100644 --- a/src/integrationTest/java/org/opensearch/security/http/ExtendedProxyAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/ExtendedProxyAuthenticationTest.java @@ -48,202 +48,213 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class ExtendedProxyAuthenticationTest extends CommonProxyAuthenticationTests { - - public static final String ID_ONE_1 = "one#1"; - public static final String ID_TWO_2 = "two#2"; - public static final Map PROXY_AUTHENTICATOR_CONFIG = Map.of( - "user_header", HEADER_PROXY_USER, - "roles_header", HEADER_PROXY_ROLES, - "attr_header_prefix", HEADER_PREFIX_CUSTOM_ATTRIBUTES - ); - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .xff(new XffConfig(true).internalProxiesRegexp("127\\.0\\.0\\.10")) - .authc(new AuthcDomain("proxy_auth_domain", -5, true) - .httpAuthenticator(new HttpAuthenticator("extended-proxy").challenge(false).config(PROXY_AUTHENTICATOR_CONFIG)) - .backend(new AuthenticationBackend("noop"))) - .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN).roles(ROLE_ALL_INDEX_SEARCH, ROLE_PERSONAL_INDEX_SEARCH) - .rolesMapping(ROLES_MAPPING_CAPTAIN, ROLES_MAPPING_FIRST_MATE).build(); - - @Override - protected LocalCluster getCluster() { - return cluster; - } - - @BeforeClass - public static void createTestData() { - try(Client client = cluster.getInternalNodeClient()){ - client.prepareIndex(PERSONAL_INDEX_NAME_SPOCK).setId(ID_ONE_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); - client.prepareIndex(PERSONAL_INDEX_NAME_KIRK).setId(ID_TWO_2).setRefreshPolicy(IMMEDIATE).setSource(SONGS[1].asMap()).get(); - } - } - - @Test - @Override - public void shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured() { - super.shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured(); - } - - @Test - @Override - public void shouldAuthenticateWithProxy_positiveUserKirk() throws IOException { - super.shouldAuthenticateWithProxy_positiveUserKirk(); - } - - @Test - @Override - public void shouldAuthenticateWithProxy_positiveUserSpock() throws IOException { - super.shouldAuthenticateWithProxy_positiveUserSpock(); - } - - @Test - @Override - public void shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing() throws IOException { - super.shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing(); - } - - @Test - @Override - public void shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing() throws IOException { - super.shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing(); - } - - @Test - @Override - public void shouldAuthenticateWithProxyWhenRolesHeaderIsMissing() throws IOException { - super.shouldAuthenticateWithProxyWhenRolesHeaderIsMissing(); - } - - @Test - @Override - public void shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy() throws IOException { - super.shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy(); - } - - @Test - @Override - public void shouldRetrieveEmptyListOfRoles() throws IOException { - super.shouldRetrieveEmptyListOfRoles(); - } - - @Test - @Override - public void shouldRetrieveSingleRoleFirstMate() throws IOException { - super.shouldRetrieveSingleRoleFirstMate(); - } - - @Test - @Override - public void shouldRetrieveSingleRoleCaptain() throws IOException { - super.shouldRetrieveSingleRoleCaptain(); - } - - @Test - @Override - public void shouldRetrieveMultipleRoles() throws IOException { - super.shouldRetrieveMultipleRoles(); - } - - // tests specific for extended proxy authentication - - @Test - public void shouldRetrieveCustomAttributeNameDepartment() throws IOException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_USER, USER_SPOCK) - .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) - .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE); - try(TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List customAttributes = response.getTextArrayFromJsonBody(POINTER_CUSTOM_ATTRIBUTES); - assertThat(customAttributes, hasSize(2)); - assertThat(customAttributes, containsInAnyOrder(USER_ATTRIBUTE_USERNAME_NAME, USER_ATTRIBUTE_DEPARTMENT_NAME)); - } - } - - @Test - public void shouldRetrieveCustomAttributeNameSkills() throws IOException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_USER, USER_SPOCK) - .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) - .header(HEADER_SKILLS, "bilocation"); - try(TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List customAttributes = response.getTextArrayFromJsonBody(POINTER_CUSTOM_ATTRIBUTES); - assertThat(customAttributes, hasSize(2)); - assertThat(customAttributes, containsInAnyOrder(USER_ATTRIBUTE_USERNAME_NAME, USER_ATTRIBUTE_SKILLS_NAME)); - } - } - - @Test - public void shouldRetrieveMultipleCustomAttributes() throws IOException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_USER, USER_SPOCK) - .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) - .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE) - .header(HEADER_SKILLS, "bilocation"); - try(TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List customAttributes = response.getTextArrayFromJsonBody(POINTER_CUSTOM_ATTRIBUTES); - assertThat(customAttributes, hasSize(3)); - assertThat(customAttributes, containsInAnyOrder( - USER_ATTRIBUTE_DEPARTMENT_NAME, - USER_ATTRIBUTE_USERNAME_NAME, - USER_ATTRIBUTE_SKILLS_NAME) - ); - } - } - - @Test - public void shouldRetrieveUserRolesAndAttributesSoThatAccessToPersonalIndexIsPossible_positive() throws UnknownHostException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_USER, USER_SPOCK) - .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) - .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE); - try(TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { - - HttpResponse response = client.get("/" + PERSONAL_INDEX_NAME_SPOCK + "/_search"); - - response.assertStatusCode(200); - assertThat(response.getLongFromJsonBody(POINTER_TOTAL_HITS), equalTo(1L)); - assertThat(response.getTextFromJsonBody(POINTER_FIRST_DOCUMENT_ID), equalTo(ID_ONE_1)); - assertThat(response.getTextFromJsonBody(POINTER_FIRST_DOCUMENT_INDEX), equalTo(PERSONAL_INDEX_NAME_SPOCK)); - assertThat(response.getTextFromJsonBody(POINTER_FIRST_DOCUMENT_SOURCE_TITLE), equalTo(TITLE_MAGNUM_OPUS)); - } - } - - @Test - public void shouldRetrieveUserRolesAndAttributesSoThatAccessToPersonalIndexIsPossible_negative() throws UnknownHostException { - TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration() - .sourceInetAddress(InetAddress.getByName(IP_PROXY)) - .header(HEADER_FORWARDED_FOR, IP_CLIENT) - .header(HEADER_PROXY_USER, USER_SPOCK) - .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) - .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE); - try(TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { - - HttpResponse response = client.get("/" + PERSONAL_INDEX_NAME_KIRK + "/_search"); - - response.assertStatusCode(403); - } - } + public static final String ID_ONE_1 = "one#1"; + public static final String ID_TWO_2 = "two#2"; + public static final Map PROXY_AUTHENTICATOR_CONFIG = Map.of( + "user_header", + HEADER_PROXY_USER, + "roles_header", + HEADER_PROXY_ROLES, + "attr_header_prefix", + HEADER_PREFIX_CUSTOM_ATTRIBUTES + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .xff(new XffConfig(true).internalProxiesRegexp("127\\.0\\.0\\.10")) + .authc( + new AuthcDomain("proxy_auth_domain", -5, true).httpAuthenticator( + new HttpAuthenticator("extended-proxy").challenge(false).config(PROXY_AUTHENTICATOR_CONFIG) + ).backend(new AuthenticationBackend("noop")) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_ADMIN) + .roles(ROLE_ALL_INDEX_SEARCH, ROLE_PERSONAL_INDEX_SEARCH) + .rolesMapping(ROLES_MAPPING_CAPTAIN, ROLES_MAPPING_FIRST_MATE) + .build(); + + @Override + protected LocalCluster getCluster() { + return cluster; + } + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + client.prepareIndex(PERSONAL_INDEX_NAME_SPOCK).setId(ID_ONE_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); + client.prepareIndex(PERSONAL_INDEX_NAME_KIRK).setId(ID_TWO_2).setRefreshPolicy(IMMEDIATE).setSource(SONGS[1].asMap()).get(); + } + } + + @Test + @Override + public void shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured() { + super.shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_positiveUserKirk() throws IOException { + super.shouldAuthenticateWithProxy_positiveUserKirk(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_positiveUserSpock() throws IOException { + super.shouldAuthenticateWithProxy_positiveUserSpock(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing() throws IOException { + super.shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing() throws IOException { + super.shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing(); + } + + @Test + @Override + public void shouldAuthenticateWithProxyWhenRolesHeaderIsMissing() throws IOException { + super.shouldAuthenticateWithProxyWhenRolesHeaderIsMissing(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy() throws IOException { + super.shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy(); + } + + @Test + @Override + public void shouldRetrieveEmptyListOfRoles() throws IOException { + super.shouldRetrieveEmptyListOfRoles(); + } + + @Test + @Override + public void shouldRetrieveSingleRoleFirstMate() throws IOException { + super.shouldRetrieveSingleRoleFirstMate(); + } + + @Test + @Override + public void shouldRetrieveSingleRoleCaptain() throws IOException { + super.shouldRetrieveSingleRoleCaptain(); + } + + @Test + @Override + public void shouldRetrieveMultipleRoles() throws IOException { + super.shouldRetrieveMultipleRoles(); + } + + // tests specific for extended proxy authentication + + @Test + public void shouldRetrieveCustomAttributeNameDepartment() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) + .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE); + try (TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List customAttributes = response.getTextArrayFromJsonBody(POINTER_CUSTOM_ATTRIBUTES); + assertThat(customAttributes, hasSize(2)); + assertThat(customAttributes, containsInAnyOrder(USER_ATTRIBUTE_USERNAME_NAME, USER_ATTRIBUTE_DEPARTMENT_NAME)); + } + } + + @Test + public void shouldRetrieveCustomAttributeNameSkills() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) + .header(HEADER_SKILLS, "bilocation"); + try (TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List customAttributes = response.getTextArrayFromJsonBody(POINTER_CUSTOM_ATTRIBUTES); + assertThat(customAttributes, hasSize(2)); + assertThat(customAttributes, containsInAnyOrder(USER_ATTRIBUTE_USERNAME_NAME, USER_ATTRIBUTE_SKILLS_NAME)); + } + } + + @Test + public void shouldRetrieveMultipleCustomAttributes() throws IOException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) + .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE) + .header(HEADER_SKILLS, "bilocation"); + try (TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List customAttributes = response.getTextArrayFromJsonBody(POINTER_CUSTOM_ATTRIBUTES); + assertThat(customAttributes, hasSize(3)); + assertThat( + customAttributes, + containsInAnyOrder(USER_ATTRIBUTE_DEPARTMENT_NAME, USER_ATTRIBUTE_USERNAME_NAME, USER_ATTRIBUTE_SKILLS_NAME) + ); + } + } + + @Test + public void shouldRetrieveUserRolesAndAttributesSoThatAccessToPersonalIndexIsPossible_positive() throws UnknownHostException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) + .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE); + try (TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { + + HttpResponse response = client.get("/" + PERSONAL_INDEX_NAME_SPOCK + "/_search"); + + response.assertStatusCode(200); + assertThat(response.getLongFromJsonBody(POINTER_TOTAL_HITS), equalTo(1L)); + assertThat(response.getTextFromJsonBody(POINTER_FIRST_DOCUMENT_ID), equalTo(ID_ONE_1)); + assertThat(response.getTextFromJsonBody(POINTER_FIRST_DOCUMENT_INDEX), equalTo(PERSONAL_INDEX_NAME_SPOCK)); + assertThat(response.getTextFromJsonBody(POINTER_FIRST_DOCUMENT_SOURCE_TITLE), equalTo(TITLE_MAGNUM_OPUS)); + } + } + + @Test + public void shouldRetrieveUserRolesAndAttributesSoThatAccessToPersonalIndexIsPossible_negative() throws UnknownHostException { + TestRestClientConfiguration testRestClientConfiguration = new TestRestClientConfiguration().sourceInetAddress( + InetAddress.getByName(IP_PROXY) + ) + .header(HEADER_FORWARDED_FOR, IP_CLIENT) + .header(HEADER_PROXY_USER, USER_SPOCK) + .header(HEADER_PROXY_ROLES, BACKEND_ROLE_CAPTAIN) + .header(HEADER_DEPARTMENT, DEPARTMENT_BRIDGE); + try (TestRestClient client = cluster.createGenericClientRestClient(testRestClientConfiguration)) { + + HttpResponse response = client.get("/" + PERSONAL_INDEX_NAME_KIRK + "/_search"); + + response.assertStatusCode(403); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java index 65a4e32d7e..5226d8854c 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthenticationTests.java @@ -19,7 +19,7 @@ import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; import org.apache.hc.core5.http.Header; -import org.apache.hc.core5.http.message.BasicHeader ; +import org.apache.hc.core5.http.message.BasicHeader; import org.junit.BeforeClass; import org.junit.ClassRule; import org.junit.Rule; @@ -67,197 +67,204 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class JwtAuthenticationTests { - public static final String CLAIM_USERNAME = "preferred-username"; - public static final String CLAIM_ROLES = "backend-user-roles"; - - public static final String USER_SUPERHERO = "superhero"; - public static final String USERNAME_ROOT = "root"; - public static final String ROLE_ADMIN = "role_admin"; - public static final String ROLE_DEVELOPER = "role_developer"; - public static final String ROLE_QA = "role_qa"; - public static final String ROLE_CTO = "role_cto"; - public static final String ROLE_CEO = "role_ceo"; - public static final String ROLE_VP = "role_vp"; - public static final String POINTER_BACKEND_ROLES = "/backend_roles"; - public static final String POINTER_USERNAME = "/user_name"; - - public static final String QA_DEPARTMENT = "qa-department"; - - public static final String CLAIM_DEPARTMENT = "department"; - - public static final String DEPARTMENT_SONG_INDEX_PATTERN = String.format("song_lyrics_${attr.jwt.%s}", CLAIM_DEPARTMENT); - - public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); - - private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); - private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); - - static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - - private static final String JWT_AUTH_HEADER = "jwt-auth"; - - private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory( - KEY_PAIR.getPrivate(), - CLAIM_USERNAME, - CLAIM_ROLES, - JWT_AUTH_HEADER); - - public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig - .AuthcDomain("jwt", BASIC_AUTH_DOMAIN_ORDER - 1) - .jwtHttpAuthenticator(new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(PUBLIC_KEY).subjectKey(CLAIM_USERNAME) - .rolesKey(CLAIM_ROLES)) - .backend("noop"); - public static final String SONG_ID_1 = "song-id-01"; - - public static final Role DEPARTMENT_SONG_LISTENER_ROLE = new Role("department-song-listener-role") - .indexPermissions("indices:data/read/search").on(DEPARTMENT_SONG_INDEX_PATTERN); - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .nodeSettings(Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() +"__" + ALL_ACCESS.getName()))) - .authc(AUTHC_HTTPBASIC_INTERNAL).users(ADMIN_USER).roles(DEPARTMENT_SONG_LISTENER_ROLE) - .authc(JWT_AUTH_DOMAIN) - .build(); - - @Rule - public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator"); - - @BeforeClass - public static void createTestData() { - try (Client client = cluster.getInternalNodeClient()) { - client.prepareIndex(QA_SONG_INDEX_NAME).setId(SONG_ID_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); - } - try(TestRestClient client = cluster.getRestClient(ADMIN_USER)){ - client.createRoleMapping(ROLE_VP, DEPARTMENT_SONG_LISTENER_ROLE.getName()); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_positive() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USER_SUPERHERO))){ - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(USER_SUPERHERO)); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_positiveWithAnotherUsername() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USERNAME_ROOT))){ - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(USERNAME_ROOT)); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_failureLackingUserName() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateTokenWithoutPreferredUsername(USER_SUPERHERO))){ - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatContainExactly("No subject found in JWT token"); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_failureExpiredToken() { - try(TestRestClient client = cluster.getRestClient(tokenFactory.generateExpiredToken(USER_SUPERHERO))){ - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatContainExactly("Invalid or expired JWT token."); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_failureIncorrectFormatOfToken() { - Header header = new BasicHeader(AUTHORIZATION, "not.a.token"); - try(TestRestClient client = cluster.getRestClient(header)){ - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatContainExactly(String.format("No JWT token found in '%s' header header", JWT_AUTH_HEADER)); - } - } - - @Test - public void shouldAuthenticateWithJwtToken_failureIncorrectSignature() { - KeyPair incorrectKeyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); - Header header = tokenFactory.generateTokenSignedWithKey(incorrectKeyPair.getPrivate(), USER_SUPERHERO); - try(TestRestClient client = cluster.getRestClient(header)){ - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatContainExactly("Invalid or expired JWT token."); - } - } - - @Test - public void shouldReadRolesFromToken_positiveFirstRoleSet() { - Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA); - try(TestRestClient client = cluster.getRestClient(header)){ - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(roles, hasSize(3)); - assertThat(roles, containsInAnyOrder(ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA)); - } - } - - @Test - public void shouldReadRolesFromToken_positiveSecondRoleSet() { - Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_CTO, ROLE_CEO, ROLE_VP); - try(TestRestClient client = cluster.getRestClient(header)){ - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(roles, hasSize(3)); - assertThat(roles, containsInAnyOrder(ROLE_CTO, ROLE_CEO, ROLE_VP)); - } - } - - @Test - public void shouldExposeTokenClaimsAsUserAttributes_positive() throws IOException { - String[] roles = { ROLE_VP }; - Map additionalClaims = Map.of(CLAIM_DEPARTMENT, QA_DEPARTMENT); - Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); - try(RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))){ - SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); - - SearchResponse response = client.search(searchRequest, DEFAULT); - - assertThat(response, isSuccessfulSearchResponse()); - assertThat(response, numberOfTotalHitsIsEqualTo(1)); - assertThat(response, searchHitsContainDocumentWithId(0, QA_SONG_INDEX_NAME, SONG_ID_1)); - assertThat(response, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); - } - } - - @Test - public void shouldExposeTokenClaimsAsUserAttributes_negative() throws IOException { - String[] roles = { ROLE_VP }; - Map additionalClaims = Map.of(CLAIM_DEPARTMENT, "department-without-access-to-qa-song-index"); - Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); - try(RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))){ - SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); - - assertThatThrownBy(() -> client.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); - } - } + public static final String CLAIM_USERNAME = "preferred-username"; + public static final String CLAIM_ROLES = "backend-user-roles"; + + public static final String USER_SUPERHERO = "superhero"; + public static final String USERNAME_ROOT = "root"; + public static final String ROLE_ADMIN = "role_admin"; + public static final String ROLE_DEVELOPER = "role_developer"; + public static final String ROLE_QA = "role_qa"; + public static final String ROLE_CTO = "role_cto"; + public static final String ROLE_CEO = "role_ceo"; + public static final String ROLE_VP = "role_vp"; + public static final String POINTER_BACKEND_ROLES = "/backend_roles"; + public static final String POINTER_USERNAME = "/user_name"; + + public static final String QA_DEPARTMENT = "qa-department"; + + public static final String CLAIM_DEPARTMENT = "department"; + + public static final String DEPARTMENT_SONG_INDEX_PATTERN = String.format("song_lyrics_${attr.jwt.%s}", CLAIM_DEPARTMENT); + + public static final String QA_SONG_INDEX_NAME = String.format("song_lyrics_%s", QA_DEPARTMENT); + + private static final KeyPair KEY_PAIR = Keys.keyPairFor(SignatureAlgorithm.RS256); + private static final String PUBLIC_KEY = new String(Base64.getEncoder().encode(KEY_PAIR.getPublic().getEncoded()), US_ASCII); + + static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + private static final String JWT_AUTH_HEADER = "jwt-auth"; + + private static final JwtAuthorizationHeaderFactory tokenFactory = new JwtAuthorizationHeaderFactory( + KEY_PAIR.getPrivate(), + CLAIM_USERNAME, + CLAIM_ROLES, + JWT_AUTH_HEADER + ); + + public static final TestSecurityConfig.AuthcDomain JWT_AUTH_DOMAIN = new TestSecurityConfig.AuthcDomain( + "jwt", + BASIC_AUTH_DOMAIN_ORDER - 1 + ).jwtHttpAuthenticator( + new JwtConfigBuilder().jwtHeader(JWT_AUTH_HEADER).signingKey(PUBLIC_KEY).subjectKey(CLAIM_USERNAME).rolesKey(CLAIM_ROLES) + ).backend("noop"); + public static final String SONG_ID_1 = "song-id-01"; + + public static final Role DEPARTMENT_SONG_LISTENER_ROLE = new Role("department-song-listener-role").indexPermissions( + "indices:data/read/search" + ).on(DEPARTMENT_SONG_INDEX_PATTERN); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .nodeSettings( + Map.of("plugins.security.restapi.roles_enabled", List.of("user_" + ADMIN_USER.getName() + "__" + ALL_ACCESS.getName())) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .roles(DEPARTMENT_SONG_LISTENER_ROLE) + .authc(JWT_AUTH_DOMAIN) + .build(); + + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.http.jwt.HTTPJwtAuthenticator"); + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + client.prepareIndex(QA_SONG_INDEX_NAME).setId(SONG_ID_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); + } + try (TestRestClient client = cluster.getRestClient(ADMIN_USER)) { + client.createRoleMapping(ROLE_VP, DEPARTMENT_SONG_LISTENER_ROLE.getName()); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_positive() { + try (TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USER_SUPERHERO))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_SUPERHERO)); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_positiveWithAnotherUsername() { + try (TestRestClient client = cluster.getRestClient(tokenFactory.generateValidToken(USERNAME_ROOT))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USERNAME_ROOT)); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_failureLackingUserName() { + try (TestRestClient client = cluster.getRestClient(tokenFactory.generateTokenWithoutPreferredUsername(USER_SUPERHERO))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatContainExactly("No subject found in JWT token"); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_failureExpiredToken() { + try (TestRestClient client = cluster.getRestClient(tokenFactory.generateExpiredToken(USER_SUPERHERO))) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatContainExactly("Invalid or expired JWT token."); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_failureIncorrectFormatOfToken() { + Header header = new BasicHeader(AUTHORIZATION, "not.a.token"); + try (TestRestClient client = cluster.getRestClient(header)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatContainExactly(String.format("No JWT token found in '%s' header header", JWT_AUTH_HEADER)); + } + } + + @Test + public void shouldAuthenticateWithJwtToken_failureIncorrectSignature() { + KeyPair incorrectKeyPair = Keys.keyPairFor(SignatureAlgorithm.RS256); + Header header = tokenFactory.generateTokenSignedWithKey(incorrectKeyPair.getPrivate(), USER_SUPERHERO); + try (TestRestClient client = cluster.getRestClient(header)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatContainExactly("Invalid or expired JWT token."); + } + } + + @Test + public void shouldReadRolesFromToken_positiveFirstRoleSet() { + Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA); + try (TestRestClient client = cluster.getRestClient(header)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(roles, hasSize(3)); + assertThat(roles, containsInAnyOrder(ROLE_ADMIN, ROLE_DEVELOPER, ROLE_QA)); + } + } + + @Test + public void shouldReadRolesFromToken_positiveSecondRoleSet() { + Header header = tokenFactory.generateValidToken(USER_SUPERHERO, ROLE_CTO, ROLE_CEO, ROLE_VP); + try (TestRestClient client = cluster.getRestClient(header)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List roles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(roles, hasSize(3)); + assertThat(roles, containsInAnyOrder(ROLE_CTO, ROLE_CEO, ROLE_VP)); + } + } + + @Test + public void shouldExposeTokenClaimsAsUserAttributes_positive() throws IOException { + String[] roles = { ROLE_VP }; + Map additionalClaims = Map.of(CLAIM_DEPARTMENT, QA_DEPARTMENT); + Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); + try (RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))) { + SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); + + SearchResponse response = client.search(searchRequest, DEFAULT); + + assertThat(response, isSuccessfulSearchResponse()); + assertThat(response, numberOfTotalHitsIsEqualTo(1)); + assertThat(response, searchHitsContainDocumentWithId(0, QA_SONG_INDEX_NAME, SONG_ID_1)); + assertThat(response, searchHitContainsFieldWithValue(0, FIELD_TITLE, TITLE_MAGNUM_OPUS)); + } + } + + @Test + public void shouldExposeTokenClaimsAsUserAttributes_negative() throws IOException { + String[] roles = { ROLE_VP }; + Map additionalClaims = Map.of(CLAIM_DEPARTMENT, "department-without-access-to-qa-song-index"); + Header header = tokenFactory.generateValidTokenWithCustomClaims(USER_SUPERHERO, roles, additionalClaims); + try (RestHighLevelClient client = cluster.getRestHighLevelClient(List.of(header))) { + SearchRequest searchRequest = queryStringQueryRequest(QA_SONG_INDEX_NAME, QUERY_TITLE_MAGNUM_OPUS); + + assertThatThrownBy(() -> client.search(searchRequest, DEFAULT), statusException(FORBIDDEN)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java index 61d87b173f..65d4e7df6f 100644 --- a/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java +++ b/src/integrationTest/java/org/opensearch/security/http/JwtAuthorizationHeaderFactory.java @@ -26,116 +26,116 @@ import static java.util.Objects.requireNonNull; class JwtAuthorizationHeaderFactory { - public static final String AUDIENCE = "OpenSearch"; - public static final String ISSUER = "test-code"; - private final PrivateKey privateKey; - - private final String usernameClaimName; - - private final String rolesClaimName; - - private final String headerName; - - public JwtAuthorizationHeaderFactory(PrivateKey privateKey, String usernameClaimName, String rolesClaimName, String headerName) { - this.privateKey = requireNonNull(privateKey, "Private key is required"); - this.usernameClaimName = requireNonNull(usernameClaimName, "Username claim name is required"); - this.rolesClaimName = requireNonNull(rolesClaimName, "Roles claim name is required."); - this.headerName = requireNonNull(headerName, "Header name is required"); - } - - Header generateValidToken(String username, String...roles) { - requireNonNull(username, "Username is required"); - Date now = new Date(); - String token = Jwts.builder() - .setClaims(customClaimsMap(username, roles)) - .setIssuer(ISSUER) - .setSubject(subject(username)) - .setAudience(AUDIENCE) - .setIssuedAt(now) - .setExpiration(new Date(now.getTime() + 3600 * 1000)) - .signWith(privateKey, RS256) - .compact(); - return toHeader(token); - } - - private Map customClaimsMap(String username, String[] roles) { - ImmutableMap.Builder builder = new ImmutableMap.Builder(); - if(StringUtils.isNoneEmpty(username)) { - builder.put(usernameClaimName, username); - } - if((roles != null) && (roles.length > 0)) { - builder.put(rolesClaimName, Arrays.stream(roles).collect(Collectors.joining(","))); - } - return builder.build(); - } - - Header generateValidTokenWithCustomClaims(String username, String[] roles, Map additionalClaims) { - requireNonNull(username, "Username is required"); - requireNonNull(additionalClaims, "Custom claims are required"); - Map claims = new HashMap<>(customClaimsMap(username, roles)); - claims.putAll(additionalClaims); - Date now = new Date(); - String token = Jwts.builder() - .setClaims(claims) - .setIssuer(ISSUER) - .setSubject(subject(username)) - .setAudience(AUDIENCE) - .setIssuedAt(now) - .setExpiration(new Date(now.getTime() + 3600 * 1000)) - .signWith(privateKey, RS256) - .compact(); - return toHeader(token); - } - - private BasicHeader toHeader(String token) { - return new BasicHeader(headerName, token); - } - - Header generateTokenWithoutPreferredUsername(String username) { - requireNonNull(username, "Username is required"); - Date now = new Date(); - String token = Jwts.builder() - .setIssuer(ISSUER) - .setSubject(username) - .setIssuedAt(now) - .setExpiration(new Date(now.getTime() + 3600 * 1000)) - .signWith(privateKey, RS256) - .compact(); - return toHeader(token); - } - - public Header generateExpiredToken(String username) { - requireNonNull(username, "Username is required"); - Date now = new Date(1000); - String token = Jwts.builder() - .setClaims(Map.of(usernameClaimName, username)) - .setIssuer(ISSUER) - .setSubject(subject(username)) - .setAudience(AUDIENCE) - .setIssuedAt(now) - .setExpiration(new Date(now.getTime() + 3600 * 1000)) - .signWith(privateKey, RS256) - .compact(); - return toHeader(token); - } - - public Header generateTokenSignedWithKey(PrivateKey key, String username) { - requireNonNull(key, "Private key is required"); - requireNonNull(username, "Username is required"); - Date now = new Date(); - String token = Jwts.builder() - .setClaims(Map.of(usernameClaimName, username)) - .setIssuer(ISSUER) - .setSubject(subject(username)) - .setAudience(AUDIENCE) - .setIssuedAt(now) - .setExpiration(new Date(now.getTime() + 3600 * 1000)) - .signWith(key, RS256) - .compact(); - return toHeader(token); - } - - private static String subject(String username) { - return "subject-" + username; - } + public static final String AUDIENCE = "OpenSearch"; + public static final String ISSUER = "test-code"; + private final PrivateKey privateKey; + + private final String usernameClaimName; + + private final String rolesClaimName; + + private final String headerName; + + public JwtAuthorizationHeaderFactory(PrivateKey privateKey, String usernameClaimName, String rolesClaimName, String headerName) { + this.privateKey = requireNonNull(privateKey, "Private key is required"); + this.usernameClaimName = requireNonNull(usernameClaimName, "Username claim name is required"); + this.rolesClaimName = requireNonNull(rolesClaimName, "Roles claim name is required."); + this.headerName = requireNonNull(headerName, "Header name is required"); + } + + Header generateValidToken(String username, String... roles) { + requireNonNull(username, "Username is required"); + Date now = new Date(); + String token = Jwts.builder() + .setClaims(customClaimsMap(username, roles)) + .setIssuer(ISSUER) + .setSubject(subject(username)) + .setAudience(AUDIENCE) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + 3600 * 1000)) + .signWith(privateKey, RS256) + .compact(); + return toHeader(token); + } + + private Map customClaimsMap(String username, String[] roles) { + ImmutableMap.Builder builder = new ImmutableMap.Builder(); + if (StringUtils.isNoneEmpty(username)) { + builder.put(usernameClaimName, username); + } + if ((roles != null) && (roles.length > 0)) { + builder.put(rolesClaimName, Arrays.stream(roles).collect(Collectors.joining(","))); + } + return builder.build(); + } + + Header generateValidTokenWithCustomClaims(String username, String[] roles, Map additionalClaims) { + requireNonNull(username, "Username is required"); + requireNonNull(additionalClaims, "Custom claims are required"); + Map claims = new HashMap<>(customClaimsMap(username, roles)); + claims.putAll(additionalClaims); + Date now = new Date(); + String token = Jwts.builder() + .setClaims(claims) + .setIssuer(ISSUER) + .setSubject(subject(username)) + .setAudience(AUDIENCE) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + 3600 * 1000)) + .signWith(privateKey, RS256) + .compact(); + return toHeader(token); + } + + private BasicHeader toHeader(String token) { + return new BasicHeader(headerName, token); + } + + Header generateTokenWithoutPreferredUsername(String username) { + requireNonNull(username, "Username is required"); + Date now = new Date(); + String token = Jwts.builder() + .setIssuer(ISSUER) + .setSubject(username) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + 3600 * 1000)) + .signWith(privateKey, RS256) + .compact(); + return toHeader(token); + } + + public Header generateExpiredToken(String username) { + requireNonNull(username, "Username is required"); + Date now = new Date(1000); + String token = Jwts.builder() + .setClaims(Map.of(usernameClaimName, username)) + .setIssuer(ISSUER) + .setSubject(subject(username)) + .setAudience(AUDIENCE) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + 3600 * 1000)) + .signWith(privateKey, RS256) + .compact(); + return toHeader(token); + } + + public Header generateTokenSignedWithKey(PrivateKey key, String username) { + requireNonNull(key, "Private key is required"); + requireNonNull(username, "Username is required"); + Date now = new Date(); + String token = Jwts.builder() + .setClaims(Map.of(usernameClaimName, username)) + .setIssuer(ISSUER) + .setSubject(subject(username)) + .setAudience(AUDIENCE) + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + 3600 * 1000)) + .signWith(key, RS256) + .compact(); + return toHeader(token); + } + + private static String subject(String username) { + return "subject-" + username; + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java index f6c85165d2..299b2cc7d2 100644 --- a/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/LdapAuthenticationTest.java @@ -52,61 +52,69 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class LdapAuthenticationTest { - private static final Logger log = LogManager.getLogger(LdapAuthenticationTest.class); - - private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - - private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); - - public static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer(TEST_CERTIFICATES.getRootCertificateData(), - TEST_CERTIFICATES.getLdapCertificateData(), LDIF_DATA); - - public static LocalCluster cluster = new LocalCluster.Builder() - .testCertificates(TEST_CERTIFICATES) - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .authc(new AuthcDomain("ldap", BASIC_AUTH_DOMAIN_ORDER + 1, true) - .httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) - .backend(new AuthenticationBackend("ldap") - .config(() -> LdapAuthenticationConfigBuilder.config() - // this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used to postpone - // execution of the code in this block. - .enableSsl(false) - .enableStartTls(false) - .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapNonTlsPort())) - .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) - .password(PASSWORD_OPEN_SEARCH) - .userBase(DN_PEOPLE_TEST_ORG) - .userSearch(USER_SEARCH) - .usernameAttribute(USERNAME_ATTRIBUTE) - .build()))) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(ADMIN_USER) - .build(); - - @ClassRule - public static RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster); - - @Rule - public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); - - @Test - public void shouldAuthenticateUserWithLdap_positive() { - try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - } - } - - @Test - public void shouldAuthenticateUserWithLdap_negativeWhenIncorrectPassword() { - try (TestRestClient client = cluster.getRestClient(USER_SPOCK, "incorrect password")) { - TestRestClient.HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - String expectedStackTraceFragment = "Unable to bind as user '".concat(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) - .concat("' because the provided password was incorrect."); - logsRule.assertThatStackTraceContain(expectedStackTraceFragment); - } - } + private static final Logger log = LogManager.getLogger(LdapAuthenticationTest.class); + + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + + private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); + + public static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer( + TEST_CERTIFICATES.getRootCertificateData(), + TEST_CERTIFICATES.getLdapCertificateData(), + LDIF_DATA + ); + + public static LocalCluster cluster = new LocalCluster.Builder().testCertificates(TEST_CERTIFICATES) + .clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authc( + new AuthcDomain("ldap", BASIC_AUTH_DOMAIN_ORDER + 1, true).httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) + .backend( + new AuthenticationBackend("ldap").config( + () -> LdapAuthenticationConfigBuilder.config() + // this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used to + // postpone + // execution of the code in this block. + .enableSsl(false) + .enableStartTls(false) + .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapNonTlsPort())) + .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .password(PASSWORD_OPEN_SEARCH) + .userBase(DN_PEOPLE_TEST_ORG) + .userSearch(USER_SEARCH) + .usernameAttribute(USERNAME_ATTRIBUTE) + .build() + ) + ) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .build(); + + @ClassRule + public static RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster); + + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); + + @Test + public void shouldAuthenticateUserWithLdap_positive() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + } + } + + @Test + public void shouldAuthenticateUserWithLdap_negativeWhenIncorrectPassword() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, "incorrect password")) { + TestRestClient.HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + String expectedStackTraceFragment = "Unable to bind as user '".concat(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) + .concat("' because the provided password was incorrect."); + logsRule.assertThatStackTraceContain(expectedStackTraceFragment); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/LdapStartTlsAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/LdapStartTlsAuthenticationTest.java index 6a117a8b5a..395467897d 100644 --- a/src/integrationTest/java/org/opensearch/security/http/LdapStartTlsAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/LdapStartTlsAuthenticationTest.java @@ -51,59 +51,68 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class LdapStartTlsAuthenticationTest { - private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); + private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); - public static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer(TEST_CERTIFICATES.getRootCertificateData(), - TEST_CERTIFICATES.getLdapCertificateData(), LDIF_DATA); + public static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer( + TEST_CERTIFICATES.getRootCertificateData(), + TEST_CERTIFICATES.getLdapCertificateData(), + LDIF_DATA + ); - public static LocalCluster cluster = new LocalCluster.Builder() - .testCertificates(TEST_CERTIFICATES) - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .authc(new AuthcDomain("ldap-config-id", BASIC_AUTH_DOMAIN_ORDER + 1, true) - .httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) - .backend(new AuthenticationBackend("ldap") - .config(() -> LdapAuthenticationConfigBuilder.config() - // this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used - .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapNonTlsPort())) - .enableSsl(false) - .enableStartTls(true) - .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) - .password(PASSWORD_OPEN_SEARCH) - .userBase(DN_PEOPLE_TEST_ORG) - .userSearch(USER_SEARCH) - .usernameAttribute(USERNAME_ATTRIBUTE) - .penTrustedCasFilePath(TEST_CERTIFICATES.getRootCertificate().getAbsolutePath()) - .build()))) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(ADMIN_USER) - .build(); + public static LocalCluster cluster = new LocalCluster.Builder().testCertificates(TEST_CERTIFICATES) + .clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authc( + new AuthcDomain("ldap-config-id", BASIC_AUTH_DOMAIN_ORDER + 1, true).httpAuthenticator( + new HttpAuthenticator("basic").challenge(false) + ) + .backend( + new AuthenticationBackend("ldap").config( + () -> LdapAuthenticationConfigBuilder.config() + // this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used + .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapNonTlsPort())) + .enableSsl(false) + .enableStartTls(true) + .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .password(PASSWORD_OPEN_SEARCH) + .userBase(DN_PEOPLE_TEST_ORG) + .userSearch(USER_SEARCH) + .usernameAttribute(USERNAME_ATTRIBUTE) + .penTrustedCasFilePath(TEST_CERTIFICATES.getRootCertificate().getAbsolutePath()) + .build() + ) + ) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .build(); - @ClassRule - public static RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster); + @ClassRule + public static RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster); - @Rule - public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); - @Test - public void shouldAuthenticateUserWithLdap_positive() { - try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { - TestRestClient.HttpResponse response = client.getAuthInfo(); + @Test + public void shouldAuthenticateUserWithLdap_positive() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { + TestRestClient.HttpResponse response = client.getAuthInfo(); - response.assertStatusCode(200); - } - } + response.assertStatusCode(200); + } + } - @Test - public void shouldAuthenticateUserWithLdap_negativeWhenIncorrectPassword() { - try (TestRestClient client = cluster.getRestClient(USER_SPOCK, "incorrect password")) { - TestRestClient.HttpResponse response = client.getAuthInfo(); + @Test + public void shouldAuthenticateUserWithLdap_negativeWhenIncorrectPassword() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, "incorrect password")) { + TestRestClient.HttpResponse response = client.getAuthInfo(); - response.assertStatusCode(401); - String expectedStackTraceFragment = "Unable to bind as user '".concat(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) - .concat("' because the provided password was incorrect."); - logsRule.assertThatStackTraceContain(expectedStackTraceFragment); - } - } + response.assertStatusCode(401); + String expectedStackTraceFragment = "Unable to bind as user '".concat(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) + .concat("' because the provided password was incorrect."); + logsRule.assertThatStackTraceContain(expectedStackTraceFragment); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java index be17b6c4cf..a27f0912e9 100644 --- a/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/LdapTlsAuthenticationTest.java @@ -90,310 +90,325 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class LdapTlsAuthenticationTest { - private static final String SONG_INDEX_NAME = "song_lyrics"; - - private static final String HEADER_NAME_IMPERSONATE = "opendistro_security_impersonate_as"; - - private static final String PERSONAL_INDEX_NAME_SPOCK = "personal-" + USER_SPOCK; - private static final String PERSONAL_INDEX_NAME_KIRK = "personal-" + USER_KIRK; - - private static final String POINTER_BACKEND_ROLES = "/backend_roles"; - private static final String POINTER_ROLES = "/roles"; - private static final String POINTER_USERNAME = "/user_name"; - private static final String POINTER_ERROR_REASON = "/error/reason"; - - private static final String SONG_ID_1 = "l0001"; - private static final String SONG_ID_2 = "l0002"; - private static final String SONG_ID_3 = "l0003"; - - private static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS); - - private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); - - private static final Role ROLE_INDEX_ADMINISTRATOR = new Role("index_administrator").indexPermissions("*").on("*"); - private static final Role ROLE_PERSONAL_INDEX_ACCESS = new Role("personal_index_access").indexPermissions("*").on("personal-${attr.ldap.uid}"); - - private static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer(TEST_CERTIFICATES.getRootCertificateData(), - TEST_CERTIFICATES.getLdapCertificateData(), LDIF_DATA); - - private static final Map USER_IMPERSONATION_CONFIGURATION = Map.of( - "plugins.security.authcz.rest_impersonation_user." + USER_KIRK, List.of(USER_SPOCK) - ); - - private static final LocalCluster cluster = new LocalCluster.Builder() - .testCertificates(TEST_CERTIFICATES) - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .nodeSettings(USER_IMPERSONATION_CONFIGURATION) - .authc(new AuthcDomain("ldap", BASIC_AUTH_DOMAIN_ORDER + 1, true) - .httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) - .backend(new AuthenticationBackend("ldap") - .config(() -> LdapAuthenticationConfigBuilder.config() - // this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used - .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapTlsPort())) - .enableSsl(true) - .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) - .password(PASSWORD_OPEN_SEARCH) - .userBase(DN_PEOPLE_TEST_ORG) - .userSearch(USER_SEARCH) - .usernameAttribute(USERNAME_ATTRIBUTE) - .penTrustedCasFilePath(TEST_CERTIFICATES.getRootCertificate().getAbsolutePath()) - .build()))) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(ADMIN_USER) - .roles(ROLE_INDEX_ADMINISTRATOR, ROLE_PERSONAL_INDEX_ACCESS) - .rolesMapping( - new RolesMapping(ROLE_INDEX_ADMINISTRATOR).backendRoles(CN_GROUP_ADMIN), - new RolesMapping(ROLE_PERSONAL_INDEX_ACCESS).backendRoles(CN_GROUP_CREW) - ) - .authz(new AuthzDomain("ldap_roles").httpEnabled(true).transportEnabled(true) - .authorizationBackend(new AuthorizationBackend("ldap") - .config(() -> new LdapAuthorizationConfigBuilder() - .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapTlsPort())) - .enableSsl(true) - .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) - .password(PASSWORD_OPEN_SEARCH) - .userBase(DN_PEOPLE_TEST_ORG) - .userSearch(USER_SEARCH) - .usernameAttribute(USERNAME_ATTRIBUTE) - .penTrustedCasFilePath(TEST_CERTIFICATES.getRootCertificate().getAbsolutePath()) - .roleBase(DN_GROUPS_TEST_ORG) - .roleSearch("(uniqueMember={0})") - .userRoleAttribute(null) - .userRoleName("disabled") - .roleName("cn") - .resolveNestedRoles(true) - .build()))) - .build(); - - @ClassRule - public static final RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster); - - @Rule - public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); - - @BeforeClass - public static void createTestData() { - try(Client client = cluster.getInternalNodeClient()){ - client.prepareIndex(SONG_INDEX_NAME).setId(SONG_ID_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); - client.prepareIndex(PERSONAL_INDEX_NAME_SPOCK).setId(SONG_ID_2).setRefreshPolicy(IMMEDIATE).setSource(SONGS[1].asMap()).get(); - client.prepareIndex(PERSONAL_INDEX_NAME_KIRK).setId(SONG_ID_3).setRefreshPolicy(IMMEDIATE).setSource(SONGS[2].asMap()).get(); - } - } - - @Test - public void shouldAuthenticateUserWithLdap_positiveSpockUser() { - try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(USER_SPOCK)); - } - } - - @Test - public void shouldAuthenticateUserWithLdap_positiveKirkUser() { - try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - String username = response.getTextFromJsonBody(POINTER_USERNAME); - assertThat(username, equalTo(USER_KIRK)); - } - } - - @Test - public void shouldAuthenticateUserWithLdap_negativeWhenIncorrectPassword() { - try (TestRestClient client = cluster.getRestClient(USER_SPOCK, "incorrect password")) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - String expectedStackTraceFragment = "Unable to bind as user '".concat(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) - .concat("' because the provided password was incorrect."); - logsRule.assertThatStackTraceContain(expectedStackTraceFragment); - } - } - - @Test - public void shouldAuthenticateUserWithLdap_negativeWhenIncorrectUsername() { - final String username = "invalid-user-name"; - try (TestRestClient client = cluster.getRestClient(username, PASSWORD_SPOCK)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatStackTraceContain(String.format("No user %s found", username)); - } - } - - @Test - public void shouldAuthenticateUserWithLdap_negativeWhenUserDoesNotExist() { - final String username = "doesNotExist"; - try (TestRestClient client = cluster.getRestClient(username, "password")) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(401); - logsRule.assertThatStackTraceContain(String.format("No user %s found", username)); - } - } - - @Test - public void shouldResolveUserRolesAgainstLdapBackend_positiveSpockUser() { - try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(backendRoles, contains(CN_GROUP_CREW)); - assertThat(response.getTextArrayFromJsonBody(POINTER_ROLES), contains(ROLE_PERSONAL_INDEX_ACCESS.getName())); - } - } - - @Test - public void shouldResolveUserRolesAgainstLdapBackend_positiveKirkUser() { - try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { - - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - assertThat(response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES), contains(CN_GROUP_ADMIN)); - assertThat(response.getTextArrayFromJsonBody(POINTER_ROLES), contains(ROLE_INDEX_ADMINISTRATOR.getName())); - } - } - - @Test - public void shouldPerformAuthorizationAgainstLdapToAccessIndex_positive() throws IOException { - try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_KIRK, PASSWORD_KIRK)) { - SearchRequest request = queryStringQueryRequest(SONG_INDEX_NAME, "*"); - - SearchResponse searchResponse = client.search(request, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, SONG_ID_1)); - } - } - - @Test - public void shouldPerformAuthorizationAgainstLdapToAccessIndex_negative() throws IOException { - try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_LEONARD, PASSWORD_LEONARD)) { - SearchRequest request = queryStringQueryRequest(SONG_INDEX_NAME, "*"); - - assertThatThrownBy(() -> client.search(request, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldResolveUserAttributesLoadedFromLdap_positive() throws IOException { - try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_SPOCK, PASSWORD_SPOCK)) { - SearchRequest request = queryStringQueryRequest(PERSONAL_INDEX_NAME_SPOCK, "*"); - - SearchResponse searchResponse = client.search(request, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, PERSONAL_INDEX_NAME_SPOCK, SONG_ID_2)); - } - } - - @Test - public void shouldResolveUserAttributesLoadedFromLdap_negative() throws IOException { - try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_SPOCK, PASSWORD_SPOCK)) { - SearchRequest request = queryStringQueryRequest(PERSONAL_INDEX_NAME_KIRK, "*"); - - assertThatThrownBy(() -> client.search(request, DEFAULT), statusException(FORBIDDEN)); - } - } - - @Test - public void shouldResolveNestedGroups_positive() { - try (TestRestClient client = cluster.getRestClient(USER_JEAN, PASSWORD_JEAN)) { - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(backendRoles, hasSize(2)); - //CN_GROUP_CREW is retrieved recursively: cn=Jean,ou=people,o=test.org -> cn=bridge,ou=groups,o=test.org -> cn=crew,ou=groups,o=test.org - assertThat(backendRoles, containsInAnyOrder(CN_GROUP_CREW, CN_GROUP_BRIDGE)); - assertThat(response.getTextArrayFromJsonBody(POINTER_ROLES), contains(ROLE_PERSONAL_INDEX_ACCESS.getName())); - } - } - - @Test - public void shouldResolveNestedGroups_negative() { - try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { - HttpResponse response = client.getAuthInfo(); - - response.assertStatusCode(200); - List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(backendRoles, not(containsInAnyOrder(CN_GROUP_CREW))); - } - } - - @Test - public void shouldImpersonateUser_positive() { - try(TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)){ - - HttpResponse response = client.getAuthInfo(new BasicHeader(HEADER_NAME_IMPERSONATE, USER_SPOCK)); - - response.assertStatusCode(200); - assertThat(response.getTextFromJsonBody(POINTER_USERNAME), equalTo(USER_SPOCK)); - List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); - assertThat(backendRoles, hasSize(1)); - assertThat(backendRoles, contains(CN_GROUP_CREW)); - } - } - - @Test - public void shouldImpersonateUser_negativeJean() { - try(TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)){ - - HttpResponse response = client.getAuthInfo(new BasicHeader(HEADER_NAME_IMPERSONATE, USER_JEAN)); - - response.assertStatusCode(403); - String expectedMessage = String.format("'%s' is not allowed to impersonate as '%s'", USER_KIRK, USER_JEAN); - assertThat(response.getTextFromJsonBody(POINTER_ERROR_REASON), equalTo(expectedMessage)); - } - } - - @Test - public void shouldImpersonateUser_negativeKirk() { - try(TestRestClient client = cluster.getRestClient(USER_JEAN, PASSWORD_JEAN)){ - - HttpResponse response = client.getAuthInfo(new BasicHeader(HEADER_NAME_IMPERSONATE, USER_KIRK)); - - response.assertStatusCode(403); - String expectedMessage = String.format("'%s' is not allowed to impersonate as '%s'", USER_JEAN, USER_KIRK); - assertThat(response.getTextFromJsonBody(POINTER_ERROR_REASON), equalTo(expectedMessage)); - } - } - - @Test - public void shouldAccessImpersonatedUserPersonalIndex_positive() throws IOException { - BasicHeader impersonateHeader = new BasicHeader(HEADER_NAME_IMPERSONATE, USER_SPOCK); - try(RestHighLevelClient client = cluster.getRestHighLevelClient(USER_KIRK, PASSWORD_KIRK, impersonateHeader)){ - SearchRequest request = queryStringQueryRequest(PERSONAL_INDEX_NAME_SPOCK, "*"); - - SearchResponse searchResponse = client.search(request, DEFAULT); - - assertThat(searchResponse, isSuccessfulSearchResponse()); - assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); - assertThat(searchResponse, searchHitsContainDocumentWithId(0, PERSONAL_INDEX_NAME_SPOCK, SONG_ID_2)); - } - } - - @Test - public void shouldAccessImpersonatedUserPersonalIndex_negative() throws IOException { - BasicHeader impersonateHeader = new BasicHeader(HEADER_NAME_IMPERSONATE, USER_SPOCK); - try(RestHighLevelClient client = cluster.getRestHighLevelClient(USER_KIRK, PASSWORD_KIRK, impersonateHeader)){ - SearchRequest request = queryStringQueryRequest(PERSONAL_INDEX_NAME_KIRK, "*"); - - assertThatThrownBy(() -> client.search(request, DEFAULT), statusException(FORBIDDEN)); - } - } + private static final String SONG_INDEX_NAME = "song_lyrics"; + + private static final String HEADER_NAME_IMPERSONATE = "opendistro_security_impersonate_as"; + + private static final String PERSONAL_INDEX_NAME_SPOCK = "personal-" + USER_SPOCK; + private static final String PERSONAL_INDEX_NAME_KIRK = "personal-" + USER_KIRK; + + private static final String POINTER_BACKEND_ROLES = "/backend_roles"; + private static final String POINTER_ROLES = "/roles"; + private static final String POINTER_USERNAME = "/user_name"; + private static final String POINTER_ERROR_REASON = "/error/reason"; + + private static final String SONG_ID_1 = "l0001"; + private static final String SONG_ID_2 = "l0002"; + private static final String SONG_ID_3 = "l0003"; + + private static final User ADMIN_USER = new User("admin").roles(ALL_ACCESS); + + private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); + + private static final Role ROLE_INDEX_ADMINISTRATOR = new Role("index_administrator").indexPermissions("*").on("*"); + private static final Role ROLE_PERSONAL_INDEX_ACCESS = new Role("personal_index_access").indexPermissions("*") + .on("personal-${attr.ldap.uid}"); + + private static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer( + TEST_CERTIFICATES.getRootCertificateData(), + TEST_CERTIFICATES.getLdapCertificateData(), + LDIF_DATA + ); + + private static final Map USER_IMPERSONATION_CONFIGURATION = Map.of( + "plugins.security.authcz.rest_impersonation_user." + USER_KIRK, + List.of(USER_SPOCK) + ); + + private static final LocalCluster cluster = new LocalCluster.Builder().testCertificates(TEST_CERTIFICATES) + .clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .nodeSettings(USER_IMPERSONATION_CONFIGURATION) + .authc( + new AuthcDomain("ldap", BASIC_AUTH_DOMAIN_ORDER + 1, true).httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) + .backend( + new AuthenticationBackend("ldap").config( + () -> LdapAuthenticationConfigBuilder.config() + // this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used + .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapTlsPort())) + .enableSsl(true) + .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .password(PASSWORD_OPEN_SEARCH) + .userBase(DN_PEOPLE_TEST_ORG) + .userSearch(USER_SEARCH) + .usernameAttribute(USERNAME_ATTRIBUTE) + .penTrustedCasFilePath(TEST_CERTIFICATES.getRootCertificate().getAbsolutePath()) + .build() + ) + ) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .roles(ROLE_INDEX_ADMINISTRATOR, ROLE_PERSONAL_INDEX_ACCESS) + .rolesMapping( + new RolesMapping(ROLE_INDEX_ADMINISTRATOR).backendRoles(CN_GROUP_ADMIN), + new RolesMapping(ROLE_PERSONAL_INDEX_ACCESS).backendRoles(CN_GROUP_CREW) + ) + .authz( + new AuthzDomain("ldap_roles").httpEnabled(true) + .transportEnabled(true) + .authorizationBackend( + new AuthorizationBackend("ldap").config( + () -> new LdapAuthorizationConfigBuilder().hosts(List.of("localhost:" + embeddedLDAPServer.getLdapTlsPort())) + .enableSsl(true) + .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .password(PASSWORD_OPEN_SEARCH) + .userBase(DN_PEOPLE_TEST_ORG) + .userSearch(USER_SEARCH) + .usernameAttribute(USERNAME_ATTRIBUTE) + .penTrustedCasFilePath(TEST_CERTIFICATES.getRootCertificate().getAbsolutePath()) + .roleBase(DN_GROUPS_TEST_ORG) + .roleSearch("(uniqueMember={0})") + .userRoleAttribute(null) + .userRoleName("disabled") + .roleName("cn") + .resolveNestedRoles(true) + .build() + ) + ) + ) + .build(); + + @ClassRule + public static final RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster); + + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); + + @BeforeClass + public static void createTestData() { + try (Client client = cluster.getInternalNodeClient()) { + client.prepareIndex(SONG_INDEX_NAME).setId(SONG_ID_1).setRefreshPolicy(IMMEDIATE).setSource(SONGS[0].asMap()).get(); + client.prepareIndex(PERSONAL_INDEX_NAME_SPOCK).setId(SONG_ID_2).setRefreshPolicy(IMMEDIATE).setSource(SONGS[1].asMap()).get(); + client.prepareIndex(PERSONAL_INDEX_NAME_KIRK).setId(SONG_ID_3).setRefreshPolicy(IMMEDIATE).setSource(SONGS[2].asMap()).get(); + } + } + + @Test + public void shouldAuthenticateUserWithLdap_positiveSpockUser() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_SPOCK)); + } + } + + @Test + public void shouldAuthenticateUserWithLdap_positiveKirkUser() { + try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + String username = response.getTextFromJsonBody(POINTER_USERNAME); + assertThat(username, equalTo(USER_KIRK)); + } + } + + @Test + public void shouldAuthenticateUserWithLdap_negativeWhenIncorrectPassword() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, "incorrect password")) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + String expectedStackTraceFragment = "Unable to bind as user '".concat(DN_CAPTAIN_SPOCK_PEOPLE_TEST_ORG) + .concat("' because the provided password was incorrect."); + logsRule.assertThatStackTraceContain(expectedStackTraceFragment); + } + } + + @Test + public void shouldAuthenticateUserWithLdap_negativeWhenIncorrectUsername() { + final String username = "invalid-user-name"; + try (TestRestClient client = cluster.getRestClient(username, PASSWORD_SPOCK)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatStackTraceContain(String.format("No user %s found", username)); + } + } + + @Test + public void shouldAuthenticateUserWithLdap_negativeWhenUserDoesNotExist() { + final String username = "doesNotExist"; + try (TestRestClient client = cluster.getRestClient(username, "password")) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(401); + logsRule.assertThatStackTraceContain(String.format("No user %s found", username)); + } + } + + @Test + public void shouldResolveUserRolesAgainstLdapBackend_positiveSpockUser() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, contains(CN_GROUP_CREW)); + assertThat(response.getTextArrayFromJsonBody(POINTER_ROLES), contains(ROLE_PERSONAL_INDEX_ACCESS.getName())); + } + } + + @Test + public void shouldResolveUserRolesAgainstLdapBackend_positiveKirkUser() { + try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { + + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + assertThat(response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES), contains(CN_GROUP_ADMIN)); + assertThat(response.getTextArrayFromJsonBody(POINTER_ROLES), contains(ROLE_INDEX_ADMINISTRATOR.getName())); + } + } + + @Test + public void shouldPerformAuthorizationAgainstLdapToAccessIndex_positive() throws IOException { + try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_KIRK, PASSWORD_KIRK)) { + SearchRequest request = queryStringQueryRequest(SONG_INDEX_NAME, "*"); + + SearchResponse searchResponse = client.search(request, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, SONG_INDEX_NAME, SONG_ID_1)); + } + } + + @Test + public void shouldPerformAuthorizationAgainstLdapToAccessIndex_negative() throws IOException { + try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_LEONARD, PASSWORD_LEONARD)) { + SearchRequest request = queryStringQueryRequest(SONG_INDEX_NAME, "*"); + + assertThatThrownBy(() -> client.search(request, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldResolveUserAttributesLoadedFromLdap_positive() throws IOException { + try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_SPOCK, PASSWORD_SPOCK)) { + SearchRequest request = queryStringQueryRequest(PERSONAL_INDEX_NAME_SPOCK, "*"); + + SearchResponse searchResponse = client.search(request, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, PERSONAL_INDEX_NAME_SPOCK, SONG_ID_2)); + } + } + + @Test + public void shouldResolveUserAttributesLoadedFromLdap_negative() throws IOException { + try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_SPOCK, PASSWORD_SPOCK)) { + SearchRequest request = queryStringQueryRequest(PERSONAL_INDEX_NAME_KIRK, "*"); + + assertThatThrownBy(() -> client.search(request, DEFAULT), statusException(FORBIDDEN)); + } + } + + @Test + public void shouldResolveNestedGroups_positive() { + try (TestRestClient client = cluster.getRestClient(USER_JEAN, PASSWORD_JEAN)) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(2)); + // CN_GROUP_CREW is retrieved recursively: cn=Jean,ou=people,o=test.org -> cn=bridge,ou=groups,o=test.org -> + // cn=crew,ou=groups,o=test.org + assertThat(backendRoles, containsInAnyOrder(CN_GROUP_CREW, CN_GROUP_BRIDGE)); + assertThat(response.getTextArrayFromJsonBody(POINTER_ROLES), contains(ROLE_PERSONAL_INDEX_ACCESS.getName())); + } + } + + @Test + public void shouldResolveNestedGroups_negative() { + try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { + HttpResponse response = client.getAuthInfo(); + + response.assertStatusCode(200); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, not(containsInAnyOrder(CN_GROUP_CREW))); + } + } + + @Test + public void shouldImpersonateUser_positive() { + try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { + + HttpResponse response = client.getAuthInfo(new BasicHeader(HEADER_NAME_IMPERSONATE, USER_SPOCK)); + + response.assertStatusCode(200); + assertThat(response.getTextFromJsonBody(POINTER_USERNAME), equalTo(USER_SPOCK)); + List backendRoles = response.getTextArrayFromJsonBody(POINTER_BACKEND_ROLES); + assertThat(backendRoles, hasSize(1)); + assertThat(backendRoles, contains(CN_GROUP_CREW)); + } + } + + @Test + public void shouldImpersonateUser_negativeJean() { + try (TestRestClient client = cluster.getRestClient(USER_KIRK, PASSWORD_KIRK)) { + + HttpResponse response = client.getAuthInfo(new BasicHeader(HEADER_NAME_IMPERSONATE, USER_JEAN)); + + response.assertStatusCode(403); + String expectedMessage = String.format("'%s' is not allowed to impersonate as '%s'", USER_KIRK, USER_JEAN); + assertThat(response.getTextFromJsonBody(POINTER_ERROR_REASON), equalTo(expectedMessage)); + } + } + + @Test + public void shouldImpersonateUser_negativeKirk() { + try (TestRestClient client = cluster.getRestClient(USER_JEAN, PASSWORD_JEAN)) { + + HttpResponse response = client.getAuthInfo(new BasicHeader(HEADER_NAME_IMPERSONATE, USER_KIRK)); + + response.assertStatusCode(403); + String expectedMessage = String.format("'%s' is not allowed to impersonate as '%s'", USER_JEAN, USER_KIRK); + assertThat(response.getTextFromJsonBody(POINTER_ERROR_REASON), equalTo(expectedMessage)); + } + } + + @Test + public void shouldAccessImpersonatedUserPersonalIndex_positive() throws IOException { + BasicHeader impersonateHeader = new BasicHeader(HEADER_NAME_IMPERSONATE, USER_SPOCK); + try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_KIRK, PASSWORD_KIRK, impersonateHeader)) { + SearchRequest request = queryStringQueryRequest(PERSONAL_INDEX_NAME_SPOCK, "*"); + + SearchResponse searchResponse = client.search(request, DEFAULT); + + assertThat(searchResponse, isSuccessfulSearchResponse()); + assertThat(searchResponse, numberOfTotalHitsIsEqualTo(1)); + assertThat(searchResponse, searchHitsContainDocumentWithId(0, PERSONAL_INDEX_NAME_SPOCK, SONG_ID_2)); + } + } + + @Test + public void shouldAccessImpersonatedUserPersonalIndex_negative() throws IOException { + BasicHeader impersonateHeader = new BasicHeader(HEADER_NAME_IMPERSONATE, USER_SPOCK); + try (RestHighLevelClient client = cluster.getRestHighLevelClient(USER_KIRK, PASSWORD_KIRK, impersonateHeader)) { + SearchRequest request = queryStringQueryRequest(PERSONAL_INDEX_NAME_KIRK, "*"); + + assertThatThrownBy(() -> client.search(request, DEFAULT), statusException(FORBIDDEN)); + } + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/ProxyAuthenticationTest.java b/src/integrationTest/java/org/opensearch/security/http/ProxyAuthenticationTest.java index be8a68fb9d..8d9ede8e5a 100644 --- a/src/integrationTest/java/org/opensearch/security/http/ProxyAuthenticationTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/ProxyAuthenticationTest.java @@ -33,89 +33,96 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class ProxyAuthenticationTest extends CommonProxyAuthenticationTests { - private static final Map PROXY_AUTHENTICATOR_CONFIG = Map.of( - "user_header", HEADER_PROXY_USER, - "roles_header", HEADER_PROXY_ROLES - ); - - @ClassRule - public static final LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .xff(new XffConfig(true).internalProxiesRegexp("127\\.0\\.0\\.10")) - .authc(new AuthcDomain("proxy_auth_domain", -5, true) - .httpAuthenticator(new HttpAuthenticator("proxy").challenge(false).config(PROXY_AUTHENTICATOR_CONFIG)) - .backend(new AuthenticationBackend("noop"))) - .authc(AUTHC_HTTPBASIC_INTERNAL).users(USER_ADMIN).roles(ROLE_ALL_INDEX_SEARCH, ROLE_PERSONAL_INDEX_SEARCH) - .rolesMapping(ROLES_MAPPING_CAPTAIN, ROLES_MAPPING_FIRST_MATE).build(); - - @Override - protected LocalCluster getCluster() { - return cluster; - } - - @Test - @Override - public void shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured() { - super.shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured(); - } - - @Test - @Override - public void shouldAuthenticateWithProxy_positiveUserKirk() throws IOException { - super.shouldAuthenticateWithProxy_positiveUserKirk(); - } - - @Test - @Override - public void shouldAuthenticateWithProxy_positiveUserSpock() throws IOException { - super.shouldAuthenticateWithProxy_positiveUserSpock(); - } - - @Test - @Override - public void shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing() throws IOException { - super.shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing(); - } - - @Test - @Override - public void shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing() throws IOException { - super.shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing(); - } - - @Test - @Override - public void shouldAuthenticateWithProxyWhenRolesHeaderIsMissing() throws IOException { - super.shouldAuthenticateWithProxyWhenRolesHeaderIsMissing(); - } - - @Test - @Override - public void shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy() throws IOException { - super.shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy(); - } - - @Test - @Override - public void shouldRetrieveEmptyListOfRoles() throws IOException { - super.shouldRetrieveEmptyListOfRoles(); - } - - @Test - @Override - public void shouldRetrieveSingleRoleFirstMate() throws IOException { - super.shouldRetrieveSingleRoleFirstMate(); - } - - @Test - @Override - public void shouldRetrieveSingleRoleCaptain() throws IOException { - super.shouldRetrieveSingleRoleCaptain(); - } - - @Test - @Override - public void shouldRetrieveMultipleRoles() throws IOException { - super.shouldRetrieveMultipleRoles(); - } + private static final Map PROXY_AUTHENTICATOR_CONFIG = Map.of( + "user_header", + HEADER_PROXY_USER, + "roles_header", + HEADER_PROXY_ROLES + ); + + @ClassRule + public static final LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .xff(new XffConfig(true).internalProxiesRegexp("127\\.0\\.0\\.10")) + .authc( + new AuthcDomain("proxy_auth_domain", -5, true).httpAuthenticator( + new HttpAuthenticator("proxy").challenge(false).config(PROXY_AUTHENTICATOR_CONFIG) + ).backend(new AuthenticationBackend("noop")) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(USER_ADMIN) + .roles(ROLE_ALL_INDEX_SEARCH, ROLE_PERSONAL_INDEX_SEARCH) + .rolesMapping(ROLES_MAPPING_CAPTAIN, ROLES_MAPPING_FIRST_MATE) + .build(); + + @Override + protected LocalCluster getCluster() { + return cluster; + } + + @Test + @Override + public void shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured() { + super.shouldAuthenticateWithBasicAuthWhenProxyAuthenticationIsConfigured(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_positiveUserKirk() throws IOException { + super.shouldAuthenticateWithProxy_positiveUserKirk(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_positiveUserSpock() throws IOException { + super.shouldAuthenticateWithProxy_positiveUserSpock(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing() throws IOException { + super.shouldAuthenticateWithProxy_negativeWhenXffHeaderIsMissing(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing() throws IOException { + super.shouldAuthenticateWithProxy_negativeWhenUserNameHeaderIsMissing(); + } + + @Test + @Override + public void shouldAuthenticateWithProxyWhenRolesHeaderIsMissing() throws IOException { + super.shouldAuthenticateWithProxyWhenRolesHeaderIsMissing(); + } + + @Test + @Override + public void shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy() throws IOException { + super.shouldAuthenticateWithProxy_negativeWhenRequestWasNotSendByProxy(); + } + + @Test + @Override + public void shouldRetrieveEmptyListOfRoles() throws IOException { + super.shouldRetrieveEmptyListOfRoles(); + } + + @Test + @Override + public void shouldRetrieveSingleRoleFirstMate() throws IOException { + super.shouldRetrieveSingleRoleFirstMate(); + } + + @Test + @Override + public void shouldRetrieveSingleRoleCaptain() throws IOException { + super.shouldRetrieveSingleRoleCaptain(); + } + + @Test + @Override + public void shouldRetrieveMultipleRoles() throws IOException { + super.shouldRetrieveMultipleRoles(); + } } diff --git a/src/integrationTest/java/org/opensearch/security/http/UntrustedLdapServerCertificateTest.java b/src/integrationTest/java/org/opensearch/security/http/UntrustedLdapServerCertificateTest.java index 87a55f4757..10e3f0853f 100644 --- a/src/integrationTest/java/org/opensearch/security/http/UntrustedLdapServerCertificateTest.java +++ b/src/integrationTest/java/org/opensearch/security/http/UntrustedLdapServerCertificateTest.java @@ -50,48 +50,55 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class UntrustedLdapServerCertificateTest { - private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); + private static final TestSecurityConfig.User ADMIN_USER = new TestSecurityConfig.User("admin").roles(ALL_ACCESS); - private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); + private static final TestCertificates TEST_CERTIFICATES = new TestCertificates(); - public static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer(TEST_CERTIFICATES.getRootCertificateData(), - TEST_CERTIFICATES.createSelfSignedCertificate("CN=untrusted"), LDIF_DATA); + public static final EmbeddedLDAPServer embeddedLDAPServer = new EmbeddedLDAPServer( + TEST_CERTIFICATES.getRootCertificateData(), + TEST_CERTIFICATES.createSelfSignedCertificate("CN=untrusted"), + LDIF_DATA + ); - public static LocalCluster cluster = new LocalCluster.Builder() - .testCertificates(TEST_CERTIFICATES) - .clusterManager(ClusterManager.SINGLENODE).anonymousAuth(false) - .authc(new AuthcDomain("ldap", BASIC_AUTH_DOMAIN_ORDER + 1, true) - .httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) - .backend(new AuthenticationBackend("ldap") - .config(() -> LdapAuthenticationConfigBuilder.config() - // this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used - .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapTlsPort())) - .enableSsl(true) - .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) - .password(PASSWORD_OPEN_SEARCH) - .userBase(DN_PEOPLE_TEST_ORG) - .userSearch(USER_SEARCH) - .usernameAttribute(USERNAME_ATTRIBUTE) - .penTrustedCasFilePath(TEST_CERTIFICATES.getRootCertificate().getAbsolutePath()) - .build()))) - .authc(AUTHC_HTTPBASIC_INTERNAL) - .users(ADMIN_USER) - .build(); + public static LocalCluster cluster = new LocalCluster.Builder().testCertificates(TEST_CERTIFICATES) + .clusterManager(ClusterManager.SINGLENODE) + .anonymousAuth(false) + .authc( + new AuthcDomain("ldap", BASIC_AUTH_DOMAIN_ORDER + 1, true).httpAuthenticator(new HttpAuthenticator("basic").challenge(false)) + .backend( + new AuthenticationBackend("ldap").config( + () -> LdapAuthenticationConfigBuilder.config() + // this port is available when embeddedLDAPServer is already started, therefore Supplier interface is used + .hosts(List.of("localhost:" + embeddedLDAPServer.getLdapTlsPort())) + .enableSsl(true) + .bindDn(DN_OPEN_SEARCH_PEOPLE_TEST_ORG) + .password(PASSWORD_OPEN_SEARCH) + .userBase(DN_PEOPLE_TEST_ORG) + .userSearch(USER_SEARCH) + .usernameAttribute(USERNAME_ATTRIBUTE) + .penTrustedCasFilePath(TEST_CERTIFICATES.getRootCertificate().getAbsolutePath()) + .build() + ) + ) + ) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(ADMIN_USER) + .build(); - @ClassRule - public static RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster); + @ClassRule + public static RuleChain ruleChain = RuleChain.outerRule(embeddedLDAPServer).around(cluster); - @Rule - public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); + @Rule + public LogsRule logsRule = new LogsRule("com.amazon.dlic.auth.ldap.backend.LDAPAuthenticationBackend"); - @Test - public void shouldNotAuthenticateUserWithLdap() { - try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { - TestRestClient.HttpResponse response = client.getAuthInfo(); + @Test + public void shouldNotAuthenticateUserWithLdap() { + try (TestRestClient client = cluster.getRestClient(USER_SPOCK, PASSWORD_SPOCK)) { + TestRestClient.HttpResponse response = client.getAuthInfo(); - response.assertStatusCode(401); - } - logsRule.assertThatStackTraceContain("javax.net.ssl.SSLHandshakeException"); - } + response.assertStatusCode(401); + } + logsRule.assertThatStackTraceContain("javax.net.ssl.SSLHandshakeException"); + } } diff --git a/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java b/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java index 9fd3765ea6..a896376d4d 100644 --- a/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java +++ b/src/integrationTest/java/org/opensearch/security/privileges/PrivilegesEvaluatorTest.java @@ -36,36 +36,36 @@ @ThreadLeakScope(ThreadLeakScope.Scope.NONE) public class PrivilegesEvaluatorTest { - protected final static TestSecurityConfig.User NEGATIVE_LOOKAHEAD = new TestSecurityConfig.User( - "negative_lookahead_user") - .roles(new Role("negative_lookahead_role").indexPermissions("read").on("/^(?!t.*).*/") - .clusterPermissions("cluster_composite_ops")); + protected final static TestSecurityConfig.User NEGATIVE_LOOKAHEAD = new TestSecurityConfig.User("negative_lookahead_user").roles( + new Role("negative_lookahead_role").indexPermissions("read").on("/^(?!t.*).*/").clusterPermissions("cluster_composite_ops") + ); - protected final static TestSecurityConfig.User NEGATED_REGEX = new TestSecurityConfig.User("negated_regex_user") - .roles(new Role("negated_regex_role").indexPermissions("read").on("/^[a-z].*/") - .clusterPermissions("cluster_composite_ops")); + protected final static TestSecurityConfig.User NEGATED_REGEX = new TestSecurityConfig.User("negated_regex_user").roles( + new Role("negated_regex_role").indexPermissions("read").on("/^[a-z].*/").clusterPermissions("cluster_composite_ops") + ); - @ClassRule - public static LocalCluster cluster = new LocalCluster.Builder() - .clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS).authc(AUTHC_HTTPBASIC_INTERNAL) - .users(NEGATIVE_LOOKAHEAD, NEGATED_REGEX).build(); + @ClassRule + public static LocalCluster cluster = new LocalCluster.Builder().clusterManager(ClusterManager.THREE_CLUSTER_MANAGERS) + .authc(AUTHC_HTTPBASIC_INTERNAL) + .users(NEGATIVE_LOOKAHEAD, NEGATED_REGEX) + .build(); - @Test - public void testNegativeLookaheadPattern() throws Exception { + @Test + public void testNegativeLookaheadPattern() throws Exception { - try (TestRestClient client = cluster.getRestClient(NEGATIVE_LOOKAHEAD)) { - assertThat(client.get("*/_search").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); - assertThat(client.get("r*/_search").getStatusCode(), equalTo(HttpStatus.SC_OK)); - } - } + try (TestRestClient client = cluster.getRestClient(NEGATIVE_LOOKAHEAD)) { + assertThat(client.get("*/_search").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + assertThat(client.get("r*/_search").getStatusCode(), equalTo(HttpStatus.SC_OK)); + } + } - @Test - public void testRegexPattern() throws Exception { + @Test + public void testRegexPattern() throws Exception { - try (TestRestClient client = cluster.getRestClient(NEGATED_REGEX)) { - assertThat(client.get("*/_search").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); - assertThat(client.get("r*/_search").getStatusCode(), equalTo(HttpStatus.SC_OK)); - } + try (TestRestClient client = cluster.getRestClient(NEGATED_REGEX)) { + assertThat(client.get("*/_search").getStatusCode(), equalTo(HttpStatus.SC_FORBIDDEN)); + assertThat(client.get("r*/_search").getStatusCode(), equalTo(HttpStatus.SC_OK)); + } - } + } } diff --git a/src/integrationTest/resources/roles.yml b/src/integrationTest/resources/roles.yml index ef4765e25f..02de9bf3d5 100644 --- a/src/integrationTest/resources/roles.yml +++ b/src/integrationTest/resources/roles.yml @@ -4,16 +4,16 @@ _meta: config_version: 2 user_admin__all_access: cluster_permissions: - - "*" - index_permissions: - - index_patterns: - - "*" - allowed_actions: - "*" + index_permissions: + - index_patterns: + - "*" + allowed_actions: + - "*" user_limited-user__limited-role: index_permissions: - - index_patterns: - - "user-${user.name}" - allowed_actions: - - "indices:data/read/search" - - "indices:data/read/get" + - index_patterns: + - "user-${user.name}" + allowed_actions: + - "indices:data/read/get" + - "indices:data/read/search" diff --git a/src/main/java/org/opensearch/security/DefaultObjectMapper.java b/src/main/java/org/opensearch/security/DefaultObjectMapper.java index 774af04bfa..fb3385629b 100644 --- a/src/main/java/org/opensearch/security/DefaultObjectMapper.java +++ b/src/main/java/org/opensearch/security/DefaultObjectMapper.java @@ -57,7 +57,7 @@ public class DefaultObjectMapper { static { objectMapper.setSerializationInclusion(Include.NON_NULL); - //objectMapper.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); + // objectMapper.enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); objectMapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); defaulOmittingObjectMapper.setSerializationInclusion(Include.NON_DEFAULT); defaulOmittingObjectMapper.enable(JsonParser.Feature.STRICT_DUPLICATE_DETECTION); @@ -75,24 +75,31 @@ public static boolean getOrDefault(Map properties, String key, b if (value == null) { return defaultValue; } else if (value instanceof Boolean) { - return (boolean)value; + return (boolean) value; } else if (value instanceof String) { - String text = ((String)value).trim(); + String text = ((String) value).trim(); if ("true".equals(text) || "True".equals(text)) { return true; } if ("false".equals(text) || "False".equals(text)) { return false; } - throw InvalidFormatException.from(null, - "Cannot deserialize value of type 'boolean' from String \"" + text + "\": only \"true\" or \"false\" recognized)", - null, Boolean.class); + throw InvalidFormatException.from( + null, + "Cannot deserialize value of type 'boolean' from String \"" + text + "\": only \"true\" or \"false\" recognized)", + null, + Boolean.class + ); } - throw MismatchedInputException.from(null, Boolean.class, "Cannot deserialize instance of 'boolean' out of '" + value + "' (Property: " + key + ")"); + throw MismatchedInputException.from( + null, + Boolean.class, + "Cannot deserialize instance of 'boolean' out of '" + value + "' (Property: " + key + ")" + ); } public static T getOrDefault(Map properties, String key, T defaultValue) { - T value = (T)properties.get(key); + T value = (T) properties.get(key); return value != null ? value : defaultValue; } @@ -172,7 +179,7 @@ public static String writeValueAsString(Object value, boolean omitDefaults) thro return AccessController.doPrivileged(new PrivilegedExceptionAction() { @Override public String run() throws Exception { - return (omitDefaults?defaulOmittingObjectMapper:objectMapper).writeValueAsString(value); + return (omitDefaults ? defaulOmittingObjectMapper : objectMapper).writeValueAsString(value); } }); } catch (final PrivilegedActionException e) { @@ -229,12 +236,11 @@ public static TypeFactory getTypeFactory() { } public static Set getFields(Class cls) { - return objectMapper - .getSerializationConfig() - .introspect(getTypeFactory().constructType(cls)) - .findProperties() - .stream() - .map(BeanPropertyDefinition::getName) - .collect(ImmutableSet.toImmutableSet()); + return objectMapper.getSerializationConfig() + .introspect(getTypeFactory().constructType(cls)) + .findProperties() + .stream() + .map(BeanPropertyDefinition::getName) + .collect(ImmutableSet.toImmutableSet()); } } diff --git a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java index 85b6798410..55d15d3e0d 100644 --- a/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java +++ b/src/main/java/org/opensearch/security/OpenSearchSecurityPlugin.java @@ -245,7 +245,8 @@ public void close() throws IOException { private final SslExceptionHandler evaluateSslExceptionHandler() { if (client || disabled || SSLConfig.isSslOnlyMode()) { - return new SslExceptionHandler(){}; + return new SslExceptionHandler() { + }; } return Objects.requireNonNull(sslExceptionHandler); @@ -275,7 +276,9 @@ public OpenSearchSecurityPlugin(final Settings settings, final Path configPath) if (disabled) { this.sslCertReloadEnabled = false; - log.warn("OpenSearch Security plugin installed but disabled. This can expose your configuration (including passwords) to the public."); + log.warn( + "OpenSearch Security plugin installed but disabled. This can expose your configuration (including passwords) to the public." + ); return; } @@ -285,7 +288,6 @@ public OpenSearchSecurityPlugin(final Settings settings, final Path configPath) return; } - demoCertHashes.add("54a92508de7a39d06242a0ffbf59414d7eb478633c719e6af03938daf6de8a1a"); demoCertHashes.add("742e4659c79d7cad89ea86aab70aea490f23bbfc7e72abd5f0a5d3fb4c84d212"); demoCertHashes.add("db1264612891406639ecd25c894f256b7c5a6b7e1d9054cbe37b77acd2ddd913"); @@ -295,7 +297,7 @@ public OpenSearchSecurityPlugin(final Settings settings, final Path configPath) demoCertHashes.add("7a355f42c90e7543a267fbe3976c02f619036f5a34ce712995a22b342d83c3ce"); demoCertHashes.add("a9b5eca1399ec8518081c0d4a21a34eec4589087ce64c04fb01a488f9ad8edc9"); - //new certs 04/2018 + // new certs 04/2018 demoCertHashes.add("d14aefe70a592d7a29e14f3ff89c3d0070c99e87d21776aa07d333ee877e758f"); demoCertHashes.add("54a70016e0837a2b0c5658d1032d7ca32e432c62c55f01a2bf5adcb69a0a7ba9"); demoCertHashes.add("bdc141ab2272c779d0f242b79063152c49e1b06a2af05e0fd90d505f2b44d5f5"); @@ -311,7 +313,7 @@ public OpenSearchSecurityPlugin(final Settings settings, final Path configPath) AccessController.doPrivileged(new PrivilegedAction() { @Override public Object run() { - if(Security.getProvider("BC") == null) { + if (Security.getProvider("BC") == null) { Security.addProvider(new BouncyCastleProvider()); } return null; @@ -323,18 +325,18 @@ public Object run() { deprecationLogger.deprecate("Setting {} is ignored.", advancedModulesEnabledKey); } - log.info("Clustername: {}", settings.get("cluster.name","opensearch")); + log.info("Clustername: {}", settings.get("cluster.name", "opensearch")); if (!transportSSLEnabled && !SSLConfig.isSslOnlyMode()) { - throw new IllegalStateException(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED+" must be set to 'true'"); + throw new IllegalStateException(SSLConfigConstants.SECURITY_SSL_TRANSPORT_ENABLED + " must be set to 'true'"); } - if(!client) { + if (!client) { final List filesWithWrongPermissions = AccessController.doPrivileged(new PrivilegedAction>() { @Override public List run() { final Path confPath = new Environment(settings, configPath).configDir().toAbsolutePath(); - if(Files.isDirectory(confPath, LinkOption.NOFOLLOW_LINKS)) { + 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) { @@ -347,9 +349,9 @@ public List run() { } }); - if(filesWithWrongPermissions != null && filesWithWrongPermissions.size() > 0) { - for(final Path p: filesWithWrongPermissions) { - if(Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS)) { + if (filesWithWrongPermissions != null && filesWithWrongPermissions.size() > 0) { + for (final Path p : filesWithWrongPermissions) { + if (Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS)) { log.warn("Directory {} has insecure file permissions (should be 0700)", p); } else { log.warn("File {} has insecure file permissions (should be 0600)", p); @@ -358,13 +360,13 @@ public List run() { } } - if(!client && !settings.getAsBoolean(ConfigConstants.SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES, false)) { - //check for demo certificates + if (!client && !settings.getAsBoolean(ConfigConstants.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).configDir().toAbsolutePath(); - if(Files.isDirectory(confPath, LinkOption.NOFOLLOW_LINKS)) { + 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) { @@ -377,11 +379,13 @@ public List run() { } }); - if(files != null) { + if (files != null) { demoCertHashes.retainAll(files); - if(!demoCertHashes.isEmpty()) { - log.error("Demo certificates found but "+ConfigConstants.SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES+" is set to false."); - throw new RuntimeException("Demo certificates found "+demoCertHashes); + if (!demoCertHashes.isEmpty()) { + log.error( + "Demo certificates found but " + ConfigConstants.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"); @@ -392,22 +396,22 @@ public List run() { private String sha256(Path p) { - if(!Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) { + if (!Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) { return ""; } - if(!Files.isReadable(p)) { - log.debug("Unreadable file "+p+" found"); + 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); + log.debug(hash + " :: " + p); return hash; } catch (Exception e) { - throw new OpenSearchSecurityException("Unable to digest file "+p, e); + throw new OpenSearchSecurityException("Unable to digest file " + p, e); } } @@ -417,79 +421,133 @@ private boolean checkFilePermissions(final Path p) { return false; } - Set perms; try { perms = Files.getPosixFilePermissions(p, LinkOption.NOFOLLOW_LINKS); } catch (Exception e) { - if(log.isDebugEnabled()) { + if (log.isDebugEnabled()) { log.debug("Cannot determine posix file permissions for {} due to {}", p, e); } - //ignore, can happen on windows + // ignore, can happen on windows return false; } - if(Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS)) { + 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)) { + 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; - //} + // 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) { + 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 && !disabled) { - handlers.addAll(super.getRestHandlers(settings, restController, clusterSettings, indexScopedSettings, settingsFilter, indexNameExpressionResolver, nodesInCluster)); + handlers.addAll( + super.getRestHandlers( + settings, + restController, + clusterSettings, + indexScopedSettings, + settingsFilter, + indexNameExpressionResolver, + nodesInCluster + ) + ); - if(!SSLConfig.isSslOnlyMode()) { - handlers.add(new SecurityInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool))); + if (!SSLConfig.isSslOnlyMode()) { + handlers.add( + new SecurityInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool)) + ); handlers.add(new SecurityHealthAction(settings, restController, Objects.requireNonNull(backendRegistry))); - handlers.add(new DashboardsInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool))); - handlers.add(new TenantInfoAction(settings, restController, Objects.requireNonNull(evaluator), Objects.requireNonNull(threadPool), - Objects.requireNonNull(cs), Objects.requireNonNull(adminDns), Objects.requireNonNull(cr))); - handlers.add(new SecurityConfigUpdateAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); - handlers.add(new SecurityWhoAmIAction(settings, restController, Objects.requireNonNull(threadPool), adminDns, configPath, principalExtractor)); + handlers.add( + new DashboardsInfoAction( + settings, + restController, + Objects.requireNonNull(evaluator), + Objects.requireNonNull(threadPool) + ) + ); + handlers.add( + new TenantInfoAction( + settings, + restController, + Objects.requireNonNull(evaluator), + Objects.requireNonNull(threadPool), + Objects.requireNonNull(cs), + Objects.requireNonNull(adminDns), + Objects.requireNonNull(cr) + ) + ); + handlers.add( + new SecurityConfigUpdateAction( + settings, + restController, + Objects.requireNonNull(threadPool), + adminDns, + configPath, + principalExtractor + ) + ); + handlers.add( + new SecurityWhoAmIAction( + settings, + restController, + Objects.requireNonNull(threadPool), + adminDns, + configPath, + principalExtractor + ) + ); handlers.addAll( - SecurityRestApiActions.getHandler( - settings, - configPath, - restController, - localClient, - adminDns, - cr, cs, principalExtractor, - evaluator, - threadPool, - Objects.requireNonNull(auditLog), sks, - Objects.requireNonNull(userService), - sslCertReloadEnabled) + SecurityRestApiActions.getHandler( + settings, + configPath, + restController, + localClient, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + Objects.requireNonNull(auditLog), + sks, + Objects.requireNonNull(userService), + sslCertReloadEnabled + ) ); log.debug("Added {} rest handler(s)", handlers.size()); } @@ -501,7 +559,7 @@ public List getRestHandlers(Settings settings, RestController restC @Override public UnaryOperator getRestHandlerWrapper(final ThreadContext threadContext) { - if(client || disabled || SSLConfig.isSslOnlyMode()) { + if (client || disabled || SSLConfig.isSslOnlyMode()) { return (rh) -> rh; } @@ -511,7 +569,7 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre @Override public List> getActions() { List> actions = new ArrayList<>(1); - if(!disabled && !SSLConfig.isSslOnlyMode()) { + if (!disabled && !SSLConfig.isSslOnlyMode()) { actions.add(new ActionHandler<>(ConfigUpdateAction.INSTANCE, TransportConfigUpdateAction.class)); actions.add(new ActionHandler<>(WhoAmIAction.INSTANCE, TransportWhoAmIAction.class)); } @@ -520,7 +578,7 @@ public UnaryOperator getRestHandlerWrapper(final ThreadContext thre @Override public void onIndexModule(IndexModule indexModule) { - //called for every index! + // called for every index! if (!disabled && !client && !SSLConfig.isSslOnlyMode()) { log.debug("Handle auditLog {} for onIndexModule() of index {}", auditLog.getClass(), indexModule.getIndex().getName()); @@ -528,8 +586,19 @@ public void onIndexModule(IndexModule indexModule) { final ComplianceIndexingOperationListener ciol = new ComplianceIndexingOperationListenerImpl(auditLog); indexModule.addIndexOperationListener(ciol); - indexModule.setReaderWrapper(indexService -> new SecurityFlsDlsIndexSearcherWrapper(indexService, settings, adminDns, cs, auditLog, ciol, evaluator, salt)); - indexModule.forceQueryCacheProvider((indexSettings,nodeCache)->new QueryCache() { + indexModule.setReaderWrapper( + indexService -> new SecurityFlsDlsIndexSearcherWrapper( + indexService, + settings, + adminDns, + cs, + auditLog, + ciol, + evaluator, + salt + ) + ); + indexModule.forceQueryCacheProvider((indexSettings, nodeCache) -> new QueryCache() { @Override public Index index() { @@ -548,17 +617,21 @@ public void clear(String reason) { @Override public Weight doCache(Weight weight, QueryCachingPolicy policy) { - final Map> allowedFlsFields = (Map>) HeaderHelper.deserializeSafeFromHeader(threadPool.getThreadContext(), - ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER); + final Map> allowedFlsFields = (Map>) HeaderHelper.deserializeSafeFromHeader( + threadPool.getThreadContext(), + ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER + ); - if(SecurityUtils.evalMap(allowedFlsFields, index().getName()) != null) { + if (SecurityUtils.evalMap(allowedFlsFields, index().getName()) != null) { return weight; } else { - final Map> maskedFieldsMap = (Map>) HeaderHelper.deserializeSafeFromHeader(threadPool.getThreadContext(), - ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER); + final Map> maskedFieldsMap = (Map>) HeaderHelper.deserializeSafeFromHeader( + threadPool.getThreadContext(), + ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER + ); - if(SecurityUtils.evalMap(maskedFieldsMap, index().getName()) != null) { + if (SecurityUtils.evalMap(maskedFieldsMap, index().getName()) != null) { return weight; } else { return nodeCache.doCache(weight, policy); @@ -578,28 +651,34 @@ public void onPreQueryPhase(SearchContext context) { @Override public void onNewReaderContext(ReaderContext readerContext) { final boolean interClusterRequest = HeaderHelper.isInterClusterRequest(threadPool.getThreadContext()); - if (Origin.LOCAL.toString().equals(threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)) - && (interClusterRequest || HeaderHelper.isDirectRequest(threadPool.getThreadContext())) + if (Origin.LOCAL.toString() + .equals(threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)) + && (interClusterRequest || HeaderHelper.isDirectRequest(threadPool.getThreadContext())) ) { readerContext.putInContext("_opendistro_security_scroll_auth_local", Boolean.TRUE); } else { - readerContext.putInContext("_opendistro_security_scroll_auth", threadPool.getThreadContext() - .getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER)); + readerContext.putInContext( + "_opendistro_security_scroll_auth", + threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER) + ); } } @Override public void onNewScrollContext(ReaderContext readerContext) { final boolean interClusterRequest = HeaderHelper.isInterClusterRequest(threadPool.getThreadContext()); - if (Origin.LOCAL.toString().equals(threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)) - && (interClusterRequest || HeaderHelper.isDirectRequest(threadPool.getThreadContext())) + if (Origin.LOCAL.toString() + .equals(threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)) + && (interClusterRequest || HeaderHelper.isDirectRequest(threadPool.getThreadContext())) ) { readerContext.putInContext("_opendistro_security_scroll_auth_local", Boolean.TRUE); } else { - readerContext.putInContext("_opendistro_security_scroll_auth", threadPool.getThreadContext() - .getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER)); + readerContext.putInContext( + "_opendistro_security_scroll_auth", + threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER) + ); } } @@ -610,8 +689,7 @@ public void validateReaderContext(ReaderContext readerContext, TransportRequest final Object _user = readerContext.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); + final User currentUser = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); if (!scrollUser.equals(currentUser)) { auditLog.logMissingPrivileges(SearchScrollAction.NAME, transportRequest, null); log.error("Wrong user {} in reader context, expected {}", scrollUser, currentUser); @@ -632,8 +710,10 @@ public void onQueryPhase(SearchContext searchContext, long tookInNanos) { return; } - final Map> maskedFieldsMap = (Map>) HeaderHelper.deserializeSafeFromHeader(threadPool.getThreadContext(), - ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER); + final Map> maskedFieldsMap = (Map>) HeaderHelper.deserializeSafeFromHeader( + threadPool.getThreadContext(), + ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER + ); final String maskedEval = SecurityUtils.evalMap(maskedFieldsMap, indexModule.getIndex().getName()); if (maskedEval != null) { final Set mf = maskedFieldsMap.get(maskedEval); @@ -663,8 +743,12 @@ public List getTransportInterceptors(NamedWriteableRegistr interceptors.add(new TransportInterceptor() { @Override - public TransportRequestHandler interceptHandler(String action, String executor, - boolean forceExecution, TransportRequestHandler actualHandler) { + public TransportRequestHandler interceptHandler( + String action, + String executor, + boolean forceExecution, + TransportRequestHandler actualHandler + ) { return new TransportRequestHandler() { @@ -682,8 +766,13 @@ public AsyncSender interceptSender(AsyncSender sender) { return new AsyncSender() { @Override - public void sendRequest(Connection connection, String action, - TransportRequest request, TransportRequestOptions options, TransportResponseHandler handler) { + public void sendRequest( + Connection connection, + String action, + TransportRequest request, + TransportRequestOptions options, + TransportResponseHandler handler + ) { si.sendRequestDecorate(sender, connection, action, request, options, handler); } }; @@ -695,73 +784,148 @@ public void sendRequest(Connection connection, Str } @Override - public Map> getTransports(Settings settings, ThreadPool threadPool, PageCacheRecycler pageCacheRecycler, - CircuitBreakerService circuitBreakerService, NamedWriteableRegistry namedWriteableRegistry, NetworkService networkService) { + public Map> getTransports( + Settings settings, + ThreadPool threadPool, + PageCacheRecycler pageCacheRecycler, + CircuitBreakerService circuitBreakerService, + NamedWriteableRegistry namedWriteableRegistry, + NetworkService networkService + ) { Map> transports = new HashMap>(); - if(SSLConfig.isSslOnlyMode()) { - return super.getTransports(settings, threadPool, pageCacheRecycler, circuitBreakerService, namedWriteableRegistry, networkService); + if (SSLConfig.isSslOnlyMode()) { + return super.getTransports( + settings, + threadPool, + pageCacheRecycler, + circuitBreakerService, + namedWriteableRegistry, + networkService + ); } if (transportSSLEnabled) { - transports.put("org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport", - () -> new SecuritySSLNettyTransport(settings, Version.CURRENT, threadPool, networkService, pageCacheRecycler, - namedWriteableRegistry, circuitBreakerService, sks, evaluateSslExceptionHandler(), sharedGroupFactory, SSLConfig)); + transports.put( + "org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport", + () -> new SecuritySSLNettyTransport( + settings, + Version.CURRENT, + threadPool, + networkService, + pageCacheRecycler, + namedWriteableRegistry, + circuitBreakerService, + sks, + evaluateSslExceptionHandler(), + sharedGroupFactory, + SSLConfig + ) + ); } return transports; } @Override - public Map> getHttpTransports(Settings settings, ThreadPool threadPool, BigArrays bigArrays, - PageCacheRecycler pageCacheRecycler, CircuitBreakerService circuitBreakerService, NamedXContentRegistry xContentRegistry, - NetworkService networkService, Dispatcher dispatcher, ClusterSettings clusterSettings) { + public Map> getHttpTransports( + Settings settings, + ThreadPool threadPool, + BigArrays bigArrays, + PageCacheRecycler pageCacheRecycler, + CircuitBreakerService circuitBreakerService, + NamedXContentRegistry xContentRegistry, + NetworkService networkService, + Dispatcher dispatcher, + ClusterSettings clusterSettings + ) { - if(SSLConfig.isSslOnlyMode()) { - return super.getHttpTransports(settings, threadPool, bigArrays, pageCacheRecycler, circuitBreakerService, xContentRegistry, - networkService, dispatcher, clusterSettings); + if (SSLConfig.isSslOnlyMode()) { + return super.getHttpTransports( + settings, + threadPool, + bigArrays, + pageCacheRecycler, + circuitBreakerService, + xContentRegistry, + networkService, + dispatcher, + clusterSettings + ); } - if(!disabled) { + if (!disabled) { if (!client && httpSSLEnabled) { - final ValidatingDispatcher validatingDispatcher = new ValidatingDispatcher(threadPool.getThreadContext(), dispatcher, - settings, configPath, evaluateSslExceptionHandler()); - //TODO close odshst - final SecurityHttpServerTransport odshst = new SecurityHttpServerTransport(settings, networkService, bigArrays, - threadPool, sks, evaluateSslExceptionHandler(), xContentRegistry, validatingDispatcher, clusterSettings, sharedGroupFactory); + final ValidatingDispatcher validatingDispatcher = new ValidatingDispatcher( + threadPool.getThreadContext(), + dispatcher, + settings, + configPath, + evaluateSslExceptionHandler() + ); + // TODO close odshst + final SecurityHttpServerTransport odshst = new SecurityHttpServerTransport( + settings, + networkService, + bigArrays, + threadPool, + sks, + evaluateSslExceptionHandler(), + xContentRegistry, + validatingDispatcher, + clusterSettings, + sharedGroupFactory + ); - return Collections.singletonMap("org.opensearch.security.http.SecurityHttpServerTransport", - () -> odshst); + return Collections.singletonMap("org.opensearch.security.http.SecurityHttpServerTransport", () -> odshst); } else if (!client) { - return Collections.singletonMap("org.opensearch.security.http.SecurityHttpServerTransport", - () -> new SecurityNonSslHttpServerTransport(settings, networkService, bigArrays, threadPool, xContentRegistry, dispatcher, clusterSettings, sharedGroupFactory)); + return Collections.singletonMap( + "org.opensearch.security.http.SecurityHttpServerTransport", + () -> new SecurityNonSslHttpServerTransport( + settings, + networkService, + bigArrays, + threadPool, + xContentRegistry, + dispatcher, + clusterSettings, + sharedGroupFactory + ) + ); } } return Collections.emptyMap(); } - - @Override - public Collection createComponents(Client localClient, ClusterService clusterService, ThreadPool threadPool, - ResourceWatcherService resourceWatcherService, ScriptService scriptService, NamedXContentRegistry xContentRegistry, - Environment environment, NodeEnvironment nodeEnvironment, NamedWriteableRegistry namedWriteableRegistry, - IndexNameExpressionResolver indexNameExpressionResolver, Supplier repositoriesServiceSupplier) { + public Collection createComponents( + Client localClient, + ClusterService clusterService, + ThreadPool threadPool, + ResourceWatcherService resourceWatcherService, + ScriptService scriptService, + NamedXContentRegistry xContentRegistry, + Environment environment, + NodeEnvironment nodeEnvironment, + NamedWriteableRegistry namedWriteableRegistry, + IndexNameExpressionResolver indexNameExpressionResolver, + Supplier repositoriesServiceSupplier + ) { SSLConfig.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); - if(SSLConfig.isSslOnlyMode()) { + if (SSLConfig.isSslOnlyMode()) { return super.createComponents( - localClient, - clusterService, - threadPool, - resourceWatcherService, - scriptService, - xContentRegistry, - environment, - nodeEnvironment, - namedWriteableRegistry, - indexNameExpressionResolver, - repositoriesServiceSupplier + localClient, + clusterService, + threadPool, + resourceWatcherService, + scriptService, + xContentRegistry, + environment, + nodeEnvironment, + namedWriteableRegistry, + indexNameExpressionResolver, + repositoriesServiceSupplier ); } @@ -775,7 +939,7 @@ public Collection createComponents(Client localClient, ClusterService cl return components; } - //Register opensearch dynamic settings + // Register opensearch dynamic settings transportPassiveAuthSetting.registerClusterSettingsChangeListener(clusterService.getClusterSettings()); final ClusterInfoHolder cih = new ClusterInfoHolder(); @@ -788,8 +952,10 @@ public Collection createComponents(Client localClient, ClusterService cl final String DEFAULT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS = DefaultInterClusterRequestEvaluator.class.getName(); InterClusterRequestEvaluator interClusterRequestEvaluator = new DefaultInterClusterRequestEvaluator(settings); - final String className = settings.get(ConfigConstants.SECURITY_INTERCLUSTER_REQUEST_EVALUATOR_CLASS, - DEFAULT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS); + final String className = settings.get( + ConfigConstants.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); @@ -803,7 +969,14 @@ public Collection createComponents(Client localClient, ClusterService cl auditLog = new NullAuditLog(); privilegesInterceptor = new PrivilegesInterceptor(resolver, clusterService, localClient, threadPool); } else { - dlsFlsValve = new DlsFlsValveImpl(settings, localClient, clusterService, resolver, xContentRegistry, threadPool.getThreadContext()); + dlsFlsValve = new DlsFlsValveImpl( + settings, + localClient, + clusterService, + resolver, + xContentRegistry, + threadPool.getThreadContext() + ); auditLog = new AuditLogImpl(settings, configPath, localClient, threadPool, resolver, clusterService, environment); privilegesInterceptor = new PrivilegesInterceptorImpl(resolver, clusterService, localClient, threadPool); } @@ -823,21 +996,39 @@ public Collection createComponents(Client localClient, ClusterService cl // DLS-FLS is enabled if not client and not disabled and not SSL only. final boolean dlsFlsEnabled = !SSLConfig.isSslOnlyMode(); - evaluator = new PrivilegesEvaluator(clusterService, threadPool, cr, resolver, auditLog, - settings, privilegesInterceptor, cih, irr, dlsFlsEnabled, namedXContentRegistry.get()); + evaluator = new PrivilegesEvaluator( + clusterService, + threadPool, + cr, + resolver, + auditLog, + settings, + privilegesInterceptor, + cih, + irr, + dlsFlsEnabled, + namedXContentRegistry.get() + ); sf = new SecurityFilter(settings, evaluator, adminDns, dlsFlsValve, auditLog, threadPool, cs, compatConfig, irr, xffResolver); final String principalExtractorClass = settings.get(SSLConfigConstants.SECURITY_SSL_TRANSPORT_PRINCIPAL_EXTRACTOR_CLASS, null); - if(principalExtractorClass == null) { + if (principalExtractorClass == null) { principalExtractor = new DefaultPrincipalExtractor(); } else { principalExtractor = ReflectionHelper.instantiatePrincipalExtractor(principalExtractorClass); } - securityRestHandler = new SecurityRestFilter(backendRegistry, auditLog, threadPool, - principalExtractor, settings, configPath, compatConfig); + securityRestHandler = new SecurityRestFilter( + backendRegistry, + auditLog, + threadPool, + principalExtractor, + settings, + configPath, + compatConfig + ); HTTPOnBehalfOfJwtAuthenticator acInstance = new HTTPOnBehalfOfJwtAuthenticator(); @@ -856,12 +1047,24 @@ public Collection createComponents(Client localClient, ClusterService cl cr.setDynamicConfigFactory(dcf); - si = new SecurityInterceptor(settings, threadPool, backendRegistry, auditLog, principalExtractor, - interClusterRequestEvaluator, cs, Objects.requireNonNull(sslExceptionHandler), Objects.requireNonNull(cih), SSLConfig); + si = new SecurityInterceptor( + settings, + threadPool, + backendRegistry, + auditLog, + principalExtractor, + interClusterRequestEvaluator, + cs, + Objects.requireNonNull(sslExceptionHandler), + Objects.requireNonNull(cih), + SSLConfig + ); components.add(principalExtractor); - // NOTE: We need to create DefaultInterClusterRequestEvaluator before creating ConfigurationRepository since the latter requires security index to be accessible which means - // communciation with other nodes is already up. However for the communication to be up, there needs to be trusted nodes_dn. Hence the base values from opensearch.yml + // NOTE: We need to create DefaultInterClusterRequestEvaluator before creating ConfigurationRepository since the latter requires + // security index to be accessible which means + // communciation with other nodes is already up. However for the communication to be up, there needs to be trusted nodes_dn. Hence + // the base values from opensearch.yml // is used to first establish trust between same cluster nodes and there after dynamic config is loaded if enabled. if (DEFAULT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS.equals(className)) { DefaultInterClusterRequestEvaluator e = (DefaultInterClusterRequestEvaluator) interClusterRequestEvaluator; @@ -877,7 +1080,6 @@ public Collection createComponents(Client localClient, ClusterService cl components.add(dcf); components.add(userService); - return components; } @@ -885,7 +1087,7 @@ public Collection createComponents(Client localClient, ClusterService cl @Override public Settings additionalSettings() { - if(disabled) { + if (disabled) { return Settings.EMPTY; } @@ -893,12 +1095,13 @@ public Settings additionalSettings() { builder.put(super.additionalSettings()); - if(!SSLConfig.isSslOnlyMode()){ + if (!SSLConfig.isSslOnlyMode()) { builder.put(NetworkModule.TRANSPORT_TYPE_KEY, "org.opensearch.security.ssl.http.netty.SecuritySSLNettyTransport"); builder.put(NetworkModule.HTTP_TYPE_KEY, "org.opensearch.security.http.SecurityHttpServerTransport"); } return builder.build(); } + @Override public List> getSettings() { List> settings = new ArrayList>(); @@ -911,80 +1114,246 @@ public List> getSettings() { settings.add(SecuritySettings.LEGACY_OPENDISTRO_SSL_DUAL_MODE_SETTING); // Protected index settings - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_KEY, ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_DEFAULT, Property.NodeScope, Property.Filtered, Property.Final)); - settings.add(Setting.listSetting(ConfigConstants.SECURITY_PROTECTED_INDICES_KEY, ConfigConstants.SECURITY_PROTECTED_INDICES_DEFAULT, Function.identity(), Property.NodeScope, Property.Filtered, Property.Final)); - settings.add(Setting.listSetting(ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_KEY, ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_DEFAULT, Function.identity(), Property.NodeScope, Property.Filtered, Property.Final)); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_KEY, + ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + ) + ); + settings.add( + Setting.listSetting( + ConfigConstants.SECURITY_PROTECTED_INDICES_KEY, + ConfigConstants.SECURITY_PROTECTED_INDICES_DEFAULT, + Function.identity(), + Property.NodeScope, + Property.Filtered, + Property.Final + ) + ); + settings.add( + Setting.listSetting( + ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_KEY, + ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_DEFAULT, + Function.identity(), + Property.NodeScope, + Property.Filtered, + Property.Final + ) + ); // System index settings - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY, ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_DEFAULT, Property.NodeScope, Property.Filtered, Property.Final)); - settings.add(Setting.listSetting(ConfigConstants.SECURITY_SYSTEM_INDICES_KEY, ConfigConstants.SECURITY_SYSTEM_INDICES_DEFAULT, Function.identity(), Property.NodeScope, Property.Filtered, Property.Final)); - - if(!SSLConfig.isSslOnlyMode()) { - settings.add(Setting.listSetting(ConfigConstants.SECURITY_AUTHCZ_ADMIN_DN, Collections.emptyList(), Function.identity(), Property.NodeScope)); //not filtered here + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY, + ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_DEFAULT, + Property.NodeScope, + Property.Filtered, + Property.Final + ) + ); + settings.add( + Setting.listSetting( + ConfigConstants.SECURITY_SYSTEM_INDICES_KEY, + ConfigConstants.SECURITY_SYSTEM_INDICES_DEFAULT, + Function.identity(), + Property.NodeScope, + Property.Filtered, + Property.Final + ) + ); + + if (!SSLConfig.isSslOnlyMode()) { + settings.add( + Setting.listSetting( + ConfigConstants.SECURITY_AUTHCZ_ADMIN_DN, + Collections.emptyList(), + Function.identity(), + Property.NodeScope + ) + ); // not filtered here settings.add(Setting.simpleString(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, Property.NodeScope, Property.Filtered)); - settings.add(Setting.groupSetting(ConfigConstants.SECURITY_AUTHCZ_IMPERSONATION_DN+".", Property.NodeScope)); //not filtered here + settings.add(Setting.groupSetting(ConfigConstants.SECURITY_AUTHCZ_IMPERSONATION_DN + ".", Property.NodeScope)); // not filtered + // here settings.add(Setting.simpleString(ConfigConstants.SECURITY_CERT_OID, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_CERT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS, Property.NodeScope, Property.Filtered)); - settings.add(Setting.listSetting(ConfigConstants.SECURITY_NODES_DN, Collections.emptyList(), Function.identity(), Property.NodeScope));//not filtered here + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_CERT_INTERCLUSTER_REQUEST_EVALUATOR_CLASS, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.listSetting(ConfigConstants.SECURITY_NODES_DN, Collections.emptyList(), Function.identity(), Property.NodeScope) + );// not filtered here - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED, false, Property.NodeScope));//not filtered here + settings.add(Setting.boolSetting(ConfigConstants.SECURITY_NODES_DN_DYNAMIC_CONFIG_ENABLED, false, Property.NodeScope));// not + // filtered + // here - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE, ConfigConstants.SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE, - Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, - Property.NodeScope, Property.Filtered)); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE, + ConfigConstants.SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, + ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, + Property.NodeScope, + Property.Filtered + ) + ); settings.add(Setting.boolSetting(ConfigConstants.SECURITY_DISABLED, false, Property.NodeScope, Property.Filtered)); settings.add(Setting.intSetting(ConfigConstants.SECURITY_CACHE_TTL_MINUTES, 60, 0, Property.NodeScope, Property.Filtered)); - //Security - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_ADVANCED_MODULES_ENABLED, true, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, true, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_DFM_EMPTY_OVERRIDES_ALL, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.groupSetting(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+".", Property.NodeScope)); //not filtered here + // Security + settings.add( + Setting.boolSetting(ConfigConstants.SECURITY_ADVANCED_MODULES_ENABLED, true, Property.NodeScope, Property.Filtered) + ); + settings.add( + Setting.boolSetting(ConfigConstants.SECURITY_ALLOW_UNSAFE_DEMOCERTIFICATES, false, Property.NodeScope, Property.Filtered) + ); + settings.add( + Setting.boolSetting(ConfigConstants.SECURITY_ALLOW_DEFAULT_INIT_SECURITYINDEX, false, Property.NodeScope, Property.Filtered) + ); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, + true, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.boolSetting(ConfigConstants.SECURITY_DFM_EMPTY_OVERRIDES_ALL, false, Property.NodeScope, Property.Filtered) + ); + settings.add(Setting.groupSetting(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + ".", Property.NodeScope)); // not + // filtered + // here settings.add(Setting.simpleString(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_DISABLE_ENVVAR_REPLACEMENT, false, Property.NodeScope, Property.Filtered)); + settings.add( + Setting.boolSetting(ConfigConstants.SECURITY_DISABLE_ENVVAR_REPLACEMENT, false, Property.NodeScope, Property.Filtered) + ); // Security - Audit settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_TYPE_DEFAULT, Property.NodeScope, Property.Filtered)); settings.add(Setting.groupSetting(ConfigConstants.SECURITY_AUDIT_CONFIG_ROUTES + ".", Property.NodeScope)); - settings.add(Setting.groupSetting(ConfigConstants.SECURITY_AUDIT_CONFIG_ENDPOINTS + ".", Property.NodeScope)); + settings.add(Setting.groupSetting(ConfigConstants.SECURITY_AUDIT_CONFIG_ENDPOINTS + ".", Property.NodeScope)); settings.add(Setting.intSetting(ConfigConstants.SECURITY_AUDIT_THREADPOOL_SIZE, 10, Property.NodeScope, Property.Filtered)); - settings.add(Setting.intSetting(ConfigConstants.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)); + settings.add( + Setting.intSetting( + ConfigConstants.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 + 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)); + 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 + ) + ); - final BiFunction> boolSettingNodeScopeFiltered = (String keyWithNamespace, Boolean value) -> Setting.boolSetting(keyWithNamespace, value, Property.NodeScope, Property.Filtered); + final BiFunction> boolSettingNodeScopeFiltered = ( + String keyWithNamespace, + Boolean value) -> Setting.boolSetting(keyWithNamespace, value, Property.NodeScope, Property.Filtered); Arrays.stream(FilterEntries.values()).map(filterEntry -> { - switch(filterEntry) { + switch (filterEntry) { case DISABLE_REST_CATEGORIES: case DISABLE_TRANSPORT_CATEGORIES: - return Setting.listSetting(filterEntry.getKeyWithNamespace(), disabledCategories, Function.identity(), Property.NodeScope); + return Setting.listSetting( + filterEntry.getKeyWithNamespace(), + disabledCategories, + Function.identity(), + Property.NodeScope + ); case IGNORE_REQUESTS: - return Setting.listSetting(filterEntry.getKeyWithNamespace(), Collections.emptyList(), Function.identity(), Property.NodeScope); + return Setting.listSetting( + filterEntry.getKeyWithNamespace(), + Collections.emptyList(), + Function.identity(), + Property.NodeScope + ); case IGNORE_USERS: - return Setting.listSetting(filterEntry.getKeyWithNamespace(), ignoredUsers, Function.identity(), Property.NodeScope); + return Setting.listSetting( + filterEntry.getKeyWithNamespace(), + ignoredUsers, + Function.identity(), + Property.NodeScope + ); // All boolean settings with default of true case ENABLE_REST: case ENABLE_TRANSPORT: @@ -999,100 +1368,416 @@ public List> getSettings() { } }).forEach(settings::add); - // Security - Audit - Sink - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_OPENSEARCH_INDEX, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_OPENSEARCH_TYPE, Property.NodeScope, Property.Filtered)); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_OPENSEARCH_INDEX, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_OPENSEARCH_TYPE, + Property.NodeScope, + Property.Filtered + ) + ); // External OpenSearch - settings.add(Setting.listSetting(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_HTTP_ENDPOINTS, Lists.newArrayList("localhost:9200"), Function.identity(), Property.NodeScope)); //not filtered here - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_VERIFY_HOSTNAMES, true, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL_CLIENT_AUTH, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_CONTENT, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_FILEPATH, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_CONTENT, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_FILEPATH, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_PASSWORD, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_CONTENT, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_FILEPATH, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_JKS_CERT_ALIAS, Property.NodeScope, Property.Filtered)); - settings.add(Setting.listSetting(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_CIPHERS, Collections.emptyList(), Function.identity(), Property.NodeScope));//not filtered here - settings.add(Setting.listSetting(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_PROTOCOLS, Collections.emptyList(), Function.identity(), Property.NodeScope));//not filtered here + settings.add( + Setting.listSetting( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_HTTP_ENDPOINTS, + Lists.newArrayList("localhost:9200"), + Function.identity(), + Property.NodeScope + ) + ); // not filtered here + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL, + false, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_VERIFY_HOSTNAMES, + true, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL_CLIENT_AUTH, + false, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_CONTENT, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_FILEPATH, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_CONTENT, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_FILEPATH, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_PASSWORD, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_CONTENT, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_FILEPATH, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_JKS_CERT_ALIAS, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.listSetting( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_CIPHERS, + Collections.emptyList(), + Function.identity(), + Property.NodeScope + ) + );// not filtered here + settings.add( + Setting.listSetting( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_PROTOCOLS, + Collections.emptyList(), + Function.identity(), + Property.NodeScope + ) + );// not filtered here // Webhooks - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_WEBHOOK_URL, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_WEBHOOK_FORMAT, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_WEBHOOK_SSL_VERIFY, true, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_FILEPATH, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_CONTENT, Property.NodeScope, Property.Filtered)); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_WEBHOOK_URL, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_WEBHOOK_FORMAT, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_WEBHOOK_SSL_VERIFY, + true, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_FILEPATH, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_CONTENT, + Property.NodeScope, + Property.Filtered + ) + ); // Log4j - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_LOG4J_LOGGER_NAME, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_LOG4J_LEVEL, Property.NodeScope, Property.Filtered)); - + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_LOG4J_LOGGER_NAME, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_LOG4J_LEVEL, + Property.NodeScope, + Property.Filtered + ) + ); // Kerberos settings.add(Setting.simpleString(ConfigConstants.SECURITY_KERBEROS_KRB5_FILEPATH, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_KERBEROS_ACCEPTOR_KEYTAB_FILEPATH, Property.NodeScope, Property.Filtered)); + settings.add( + Setting.simpleString(ConfigConstants.SECURITY_KERBEROS_ACCEPTOR_KEYTAB_FILEPATH, Property.NodeScope, Property.Filtered) + ); settings.add(Setting.simpleString(ConfigConstants.SECURITY_KERBEROS_ACCEPTOR_PRINCIPAL, Property.NodeScope, Property.Filtered)); - // OpenSearch Security - REST API - settings.add(Setting.listSetting(ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED, Collections.emptyList(), Function.identity(), Property.NodeScope)); //not filtered here + settings.add( + Setting.listSetting( + ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED, + Collections.emptyList(), + Function.identity(), + Property.NodeScope + ) + ); // not filtered here settings.add(Setting.groupSetting(ConfigConstants.SECURITY_RESTAPI_ENDPOINTS_DISABLED + ".", Property.NodeScope)); settings.add(Setting.boolSetting(ConfigConstants.SECURITY_RESTAPI_ADMIN_ENABLED, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, Property.NodeScope, Property.Filtered)); - settings.add(Setting.simpleString(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, Property.NodeScope, Property.Filtered)); + settings.add( + Setting.simpleString(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, Property.NodeScope, Property.Filtered) + ); + settings.add( + Setting.simpleString( + ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, + Property.NodeScope, + Property.Filtered + ) + ); settings.add( - Setting.intSetting( - ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, - -1, -1, Property.NodeScope, Property.Filtered) + Setting.intSetting(ConfigConstants.SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, -1, -1, Property.NodeScope, Property.Filtered) ); settings.add( - Setting.simpleString( - ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH, - PasswordValidator.ScoreStrength.STRONG.name(), - PasswordValidator.ScoreStrength::fromConfiguration, - Property.NodeScope, Property.Filtered - ) + Setting.simpleString( + ConfigConstants.SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH, + PasswordValidator.ScoreStrength.STRONG.name(), + PasswordValidator.ScoreStrength::fromConfiguration, + 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.SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.listSetting(ConfigConstants.SECURITY_COMPLIANCE_IMMUTABLE_INDICES, Collections.emptyList(), Function.identity(), Property.NodeScope)); //not filtered here + 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.SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION, + false, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.listSetting( + ConfigConstants.SECURITY_COMPLIANCE_IMMUTABLE_INDICES, + Collections.emptyList(), + Function.identity(), + Property.NodeScope + ) + ); // not filtered here settings.add(Setting.simpleString(ConfigConstants.SECURITY_COMPLIANCE_SALT, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED, false, Property.NodeScope, Property.Filtered)); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED, + false, + Property.NodeScope, + Property.Filtered + ) + ); settings.add(transportPassiveAuthSetting.getDynamicSetting()); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS, false, Property.NodeScope, - Property.Filtered)); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS, + false, + Property.NodeScope, + Property.Filtered + ) + ); - //compat - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY, false, Property.NodeScope, Property.Filtered)); + // compat + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY, + false, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY, + false, + Property.NodeScope, + Property.Filtered + ) + ); // system integration - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_UNSUPPORTED_ALLOW_NOW_IN_DLS, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES, true, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED, false, Property.NodeScope, Property.Filtered)); - settings.add(Setting.boolSetting(ConfigConstants.SECURITY_UNSUPPORTED_ACCEPT_INVALID_CONFIG, false, Property.NodeScope, Property.Filtered)); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED, + false, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.boolSetting(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false, Property.NodeScope, Property.Filtered) + ); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_UNSUPPORTED_INJECT_ADMIN_USER_ENABLED, + false, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.boolSetting(ConfigConstants.SECURITY_UNSUPPORTED_ALLOW_NOW_IN_DLS, false, Property.NodeScope, Property.Filtered) + ); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, + false, + Property.NodeScope, + Property.Filtered + ) + ); + settings.add( + Setting.boolSetting(ConfigConstants.SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES, true, Property.NodeScope, Property.Filtered) + ); + settings.add( + Setting.boolSetting(ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED, false, Property.NodeScope, Property.Filtered) + ); + settings.add( + Setting.boolSetting( + ConfigConstants.SECURITY_UNSUPPORTED_ACCEPT_INVALID_CONFIG, + false, + Property.NodeScope, + Property.Filtered + ) + ); } return settings; @@ -1102,7 +1787,7 @@ public List> getSettings() { public List getSettingsFilter() { List settingsFilter = new ArrayList<>(); - if(disabled) { + if (disabled) { return settingsFilter; } settingsFilter.add("opendistro_security.*"); @@ -1113,16 +1798,16 @@ public List getSettingsFilter() { @Override public void onNodeStarted() { log.info("Node started"); - if(!SSLConfig.isSslOnlyMode() && !client && !disabled) { + if (!SSLConfig.isSslOnlyMode() && !client && !disabled) { cr.initOnNodeStart(); } final Set securityModules = ReflectionHelper.getModulesLoaded(); log.info("{} OpenSearch Security modules loaded so far: {}", securityModules.size(), securityModules); } - //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 + // 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() { @@ -1142,8 +1827,10 @@ public Function> getFieldFilter() { if (threadPool == null) { return field -> true; } - final Map> allowedFlsFields = (Map>) HeaderHelper - .deserializeSafeFromHeader(threadPool.getThreadContext(), ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER); + final Map> allowedFlsFields = (Map>) HeaderHelper.deserializeSafeFromHeader( + threadPool.getThreadContext(), + ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER + ); final String eval = SecurityUtils.evalMap(allowedFlsFields, index); @@ -1155,7 +1842,6 @@ public Function> getFieldFilter() { 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); @@ -1179,14 +1865,17 @@ public Function> getFieldFilter() { @Override public Collection getSystemIndexDescriptors(Settings settings) { - final String indexPattern = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + final String indexPattern = settings.get( + ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + ); final SystemIndexDescriptor systemIndexDescriptor = new SystemIndexDescriptor(indexPattern, "Security index"); return Collections.singletonList(systemIndexDescriptor); } private static String handleKeyword(final String field) { - if(field != null && field.endsWith(KEYWORD)) { - return field.substring(0, field.length()-KEYWORD.length()); + if (field != null && field.endsWith(KEYWORD)) { + return field.substring(0, field.length() - KEYWORD.length()); } return field; } @@ -1202,8 +1891,13 @@ public static class GuiceHolder implements LifecycleComponent { private static ExtensionsManager extensionsManager; @Inject - public GuiceHolder(final RepositoriesService repositoriesService, - final TransportService remoteClusterService, IndicesService indicesService, PitService pitService, ExtensionsManager extensionsManager) { + public GuiceHolder( + final RepositoriesService repositoriesService, + final TransportService remoteClusterService, + IndicesService indicesService, + PitService pitService, + ExtensionsManager extensionsManager + ) { GuiceHolder.repositoriesService = repositoriesService; GuiceHolder.remoteClusterService = remoteClusterService.getRemoteClusterService(); GuiceHolder.indicesService = indicesService; @@ -1224,16 +1918,18 @@ public static IndicesService getIndicesService() { return indicesService; } - public static PitService getPitService() { return pitService; } + public static PitService getPitService() { + return pitService; + } // CS-SUPPRESS-SINGLE: RegexpSingleline Extensions manager used to allow/disallow TLS connections to extensions - public static ExtensionsManager getExtensionsManager() { return extensionsManager; } + public static ExtensionsManager getExtensionsManager() { + return extensionsManager; + } // CS-ENFORCE-SINGLE - @Override - public void close() { - } + public void close() {} @Override public State lifecycleState() { @@ -1241,20 +1937,16 @@ public State lifecycleState() { } @Override - public void addLifecycleListener(LifecycleListener listener) { - } + public void addLifecycleListener(LifecycleListener listener) {} @Override - public void removeLifecycleListener(LifecycleListener listener) { - } + public void removeLifecycleListener(LifecycleListener listener) {} @Override - public void start() { - } + public void start() {} @Override - public void stop() { - } + public void stop() {} } } diff --git a/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateNodeResponse.java b/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateNodeResponse.java index 0e6944e9c4..0ac4459102 100644 --- a/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateNodeResponse.java +++ b/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateNodeResponse.java @@ -58,7 +58,7 @@ public static ConfigUpdateNodeResponse readNodeResponse(StreamInput in) throws I } public String[] getUpdatedConfigTypes() { - return updatedConfigTypes==null?null:Arrays.copyOf(updatedConfigTypes, updatedConfigTypes.length); + return updatedConfigTypes == null ? null : Arrays.copyOf(updatedConfigTypes, updatedConfigTypes.length); } public String getMessage() { @@ -81,7 +81,7 @@ public String toString() { public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { builder.startObject(); builder.field("updated_config_types", updatedConfigTypes); - builder.field("updated_config_size", updatedConfigTypes == null ? 0: updatedConfigTypes.length); + builder.field("updated_config_size", updatedConfigTypes == null ? 0 : updatedConfigTypes.length); builder.field("message", message); builder.endObject(); return builder; diff --git a/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateRequest.java b/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateRequest.java index aeb92fa057..5310c497a2 100644 --- a/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateRequest.java +++ b/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateRequest.java @@ -47,8 +47,8 @@ public ConfigUpdateRequest() { } public ConfigUpdateRequest(String[] configTypes) { - this(); - setConfigTypes(configTypes); + this(); + setConfigTypes(configTypes); } @Override diff --git a/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateRequestBuilder.java b/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateRequestBuilder.java index d33c0fa7a6..edfe1f10bb 100644 --- a/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateRequestBuilder.java +++ b/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateRequestBuilder.java @@ -30,8 +30,10 @@ import org.opensearch.action.support.nodes.NodesOperationRequestBuilder; import org.opensearch.client.OpenSearchClient; -public class ConfigUpdateRequestBuilder extends -NodesOperationRequestBuilder { +public class ConfigUpdateRequestBuilder extends NodesOperationRequestBuilder< + ConfigUpdateRequest, + ConfigUpdateResponse, + ConfigUpdateRequestBuilder> { protected ConfigUpdateRequestBuilder(OpenSearchClient client, ActionType action) { super(client, action, new ConfigUpdateRequest()); diff --git a/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateResponse.java b/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateResponse.java index 3a57ca4144..fd3176c016 100644 --- a/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateResponse.java +++ b/src/main/java/org/opensearch/security/action/configupdate/ConfigUpdateResponse.java @@ -57,15 +57,15 @@ public void writeNodesTo(final StreamOutput out, List out.writeList(nodes); } - @Override - public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject("configupdate_response"); - builder.field("nodes", getNodesMap()); - builder.field("node_size", getNodes().size()); - builder.field("has_failures", hasFailures()); - builder.field("failures_size", failures().size()); - builder.endObject(); + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject("configupdate_response"); + builder.field("nodes", getNodesMap()); + builder.field("node_size", getNodes().size()); + builder.field("has_failures", hasFailures()); + builder.field("failures_size", failures().size()); + builder.endObject(); - return builder; - } + return builder; + } } diff --git a/src/main/java/org/opensearch/security/action/configupdate/TransportConfigUpdateAction.java b/src/main/java/org/opensearch/security/action/configupdate/TransportConfigUpdateAction.java index c5c60cf8a6..9a8cc22624 100644 --- a/src/main/java/org/opensearch/security/action/configupdate/TransportConfigUpdateAction.java +++ b/src/main/java/org/opensearch/security/action/configupdate/TransportConfigUpdateAction.java @@ -49,9 +49,11 @@ import org.opensearch.transport.TransportRequest; import org.opensearch.transport.TransportService; -public class TransportConfigUpdateAction -extends -TransportNodesAction { +public class TransportConfigUpdateAction extends TransportNodesAction< + ConfigUpdateRequest, + ConfigUpdateResponse, + TransportConfigUpdateAction.NodeConfigUpdateRequest, + ConfigUpdateNodeResponse> { protected Logger logger = LogManager.getLogger(getClass()); private final Provider backendRegistry; @@ -59,13 +61,27 @@ public class TransportConfigUpdateAction private DynamicConfigFactory dynamicConfigFactory; @Inject - public TransportConfigUpdateAction(final Settings settings, - final ThreadPool threadPool, final ClusterService clusterService, final TransportService transportService, - final ConfigurationRepository configurationRepository, final ActionFilters actionFilters, - Provider backendRegistry, DynamicConfigFactory dynamicConfigFactory) { - super(ConfigUpdateAction.NAME, threadPool, clusterService, transportService, actionFilters, - ConfigUpdateRequest::new, TransportConfigUpdateAction.NodeConfigUpdateRequest::new, - ThreadPool.Names.MANAGEMENT, ConfigUpdateNodeResponse.class); + public TransportConfigUpdateAction( + final Settings settings, + final ThreadPool threadPool, + final ClusterService clusterService, + final TransportService transportService, + final ConfigurationRepository configurationRepository, + final ActionFilters actionFilters, + Provider backendRegistry, + DynamicConfigFactory dynamicConfigFactory + ) { + super( + ConfigUpdateAction.NAME, + threadPool, + clusterService, + transportService, + actionFilters, + ConfigUpdateRequest::new, + TransportConfigUpdateAction.NodeConfigUpdateRequest::new, + ThreadPool.Names.MANAGEMENT, + ConfigUpdateNodeResponse.class + ); this.configurationRepository = configurationRepository; this.backendRegistry = backendRegistry; @@ -76,7 +92,7 @@ public static class NodeConfigUpdateRequest extends TransportRequest { ConfigUpdateRequest request; - public NodeConfigUpdateRequest(StreamInput in) throws IOException{ + public NodeConfigUpdateRequest(StreamInput in) throws IOException { super(in); request = new ConfigUpdateRequest(in); } @@ -98,8 +114,11 @@ protected ConfigUpdateNodeResponse newNodeResponse(StreamInput in) throws IOExce } @Override - protected ConfigUpdateResponse newResponse(ConfigUpdateRequest request, List responses, - List failures) { + protected ConfigUpdateResponse newResponse( + ConfigUpdateRequest request, + List responses, + List failures + ) { return new ConfigUpdateResponse(this.clusterService.getClusterName(), responses, failures); } diff --git a/src/main/java/org/opensearch/security/action/whoami/TransportWhoAmIAction.java b/src/main/java/org/opensearch/security/action/whoami/TransportWhoAmIAction.java index 901800f0a2..bd3ecf46a2 100644 --- a/src/main/java/org/opensearch/security/action/whoami/TransportWhoAmIAction.java +++ b/src/main/java/org/opensearch/security/action/whoami/TransportWhoAmIAction.java @@ -40,17 +40,20 @@ import org.opensearch.threadpool.ThreadPool; import org.opensearch.transport.TransportService; -public class TransportWhoAmIAction -extends -HandledTransportAction { +public class TransportWhoAmIAction extends HandledTransportAction { private final AdminDNs adminDNs; private final ThreadPool threadPool; @Inject - public TransportWhoAmIAction(final Settings settings, - final ThreadPool threadPool, final ClusterService clusterService, final TransportService transportService, - final AdminDNs adminDNs, final ActionFilters actionFilters) { + public TransportWhoAmIAction( + final Settings settings, + final ThreadPool threadPool, + final ClusterService clusterService, + final TransportService transportService, + final AdminDNs adminDNs, + final ActionFilters actionFilters + ) { super(WhoAmIAction.NAME, transportService, actionFilters, WhoAmIRequest::new); @@ -58,15 +61,16 @@ public TransportWhoAmIAction(final Settings settings, this.threadPool = threadPool; } - @Override protected void doExecute(Task task, 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 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()); + 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/org/opensearch/security/action/whoami/WhoAmIRequestBuilder.java b/src/main/java/org/opensearch/security/action/whoami/WhoAmIRequestBuilder.java index 7e22cc000d..229ba57ed9 100644 --- a/src/main/java/org/opensearch/security/action/whoami/WhoAmIRequestBuilder.java +++ b/src/main/java/org/opensearch/security/action/whoami/WhoAmIRequestBuilder.java @@ -32,8 +32,7 @@ import org.opensearch.client.ClusterAdminClient; import org.opensearch.client.OpenSearchClient; -public class WhoAmIRequestBuilder extends -ActionRequestBuilder { +public class WhoAmIRequestBuilder extends ActionRequestBuilder { public WhoAmIRequestBuilder(final ClusterAdminClient client) throws IOException { this(client, WhoAmIAction.INSTANCE); } diff --git a/src/main/java/org/opensearch/security/action/whoami/WhoAmIResponse.java b/src/main/java/org/opensearch/security/action/whoami/WhoAmIResponse.java index 2b25344f76..3e9d74fe25 100644 --- a/src/main/java/org/opensearch/security/action/whoami/WhoAmIResponse.java +++ b/src/main/java/org/opensearch/security/action/whoami/WhoAmIResponse.java @@ -50,7 +50,6 @@ public WhoAmIResponse(String dn, boolean isAdmin, boolean isAuthenticated, boole this.isNodeCertificateRequest = isNodeCertificateRequest; } - public WhoAmIResponse() { super(); this.dn = null; @@ -106,6 +105,6 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws @Override public String toString() { - return Strings.toString(XContentType.JSON,this, true, true); + return Strings.toString(XContentType.JSON, this, true, true); } } diff --git a/src/main/java/org/opensearch/security/auditlog/AuditLog.java b/src/main/java/org/opensearch/security/auditlog/AuditLog.java index 128944e387..3ac7e095aa 100644 --- a/src/main/java/org/opensearch/security/auditlog/AuditLog.java +++ b/src/main/java/org/opensearch/security/auditlog/AuditLog.java @@ -43,30 +43,38 @@ public interface AuditLog extends Closeable { - //login + // login void logFailedLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request); + void logSucceededLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request); - //privs + // privs void logMissingPrivileges(String privilege, String effectiveUser, RestRequest request); + void logGrantedPrivileges(String effectiveUser, RestRequest request); + void logMissingPrivileges(String privilege, TransportRequest request, Task task); + void logGrantedPrivileges(String privilege, TransportRequest request, Task task); // index event requests void logIndexEvent(String privilege, TransportRequest request, Task task); - //spoof + // 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); + void logDocumentWritten(ShardId shardId, GetResult originalIndex, Index currentIndex, IndexResult result); + void logDocumentDeleted(ShardId shardId, Delete delete, DeleteResult result); // compliance config @@ -76,10 +84,14 @@ public interface AuditLog extends Closeable { void setConfig(AuditConfig auditConfig); public enum Origin { - REST, TRANSPORT, LOCAL + REST, + TRANSPORT, + LOCAL } public enum Operation { - CREATE, UPDATE, DELETE + CREATE, + UPDATE, + DELETE } } diff --git a/src/main/java/org/opensearch/security/auditlog/AuditLogSslExceptionHandler.java b/src/main/java/org/opensearch/security/auditlog/AuditLogSslExceptionHandler.java index 3421bc4a4f..942f06804f 100644 --- a/src/main/java/org/opensearch/security/auditlog/AuditLogSslExceptionHandler.java +++ b/src/main/java/org/opensearch/security/auditlog/AuditLogSslExceptionHandler.java @@ -32,7 +32,7 @@ import org.opensearch.tasks.Task; import org.opensearch.transport.TransportRequest; -public class AuditLogSslExceptionHandler implements SslExceptionHandler{ +public class AuditLogSslExceptionHandler implements SslExceptionHandler { private final AuditLog auditLog; @@ -44,14 +44,14 @@ public AuditLogSslExceptionHandler(final 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; + case 0: + auditLog.logSSLException(request, t); + break; + case 1: + auditLog.logBadHeaders(request); + break; + default: + break; } } @@ -67,18 +67,18 @@ public void logError(Throwable t, boolean isRest) { @Override public void logError(Throwable t, TransportRequest request, String action, Task task, int type) { switch (type) { - case 0: - if(t instanceof OpenSearchException) { - auditLog.logMissingPrivileges(action, request, task); - } else { - auditLog.logSSLException(request, t, action, task); - } - break; - case 1: - auditLog.logBadHeaders(request, action, task); - break; - default: - break; + case 0: + if (t instanceof OpenSearchException) { + 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/org/opensearch/security/auditlog/NullAuditLog.java b/src/main/java/org/opensearch/security/auditlog/NullAuditLog.java index ced4d0aae6..7fe3324d2e 100644 --- a/src/main/java/org/opensearch/security/auditlog/NullAuditLog.java +++ b/src/main/java/org/opensearch/security/auditlog/NullAuditLog.java @@ -45,82 +45,82 @@ public class NullAuditLog implements AuditLog { @Override public void close() throws IOException { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logFailedLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logSucceededLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logMissingPrivileges(String privilege, TransportRequest request, Task task) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logGrantedPrivileges(String privilege, TransportRequest request, Task task) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logIndexEvent(String privilege, TransportRequest request, Task task) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logBadHeaders(TransportRequest request, String action, Task task) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logBadHeaders(RestRequest request) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logSecurityIndexAttempt(TransportRequest request, String action, Task task) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logSSLException(TransportRequest request, Throwable t, String action, Task task) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logSSLException(RestRequest request, Throwable t) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logMissingPrivileges(String privilege, String effectiveUser, RestRequest request) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logGrantedPrivileges(String effectiveUser, RestRequest request) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logDocumentRead(String index, String id, ShardId shardId, Map fieldNameValues) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logDocumentWritten(ShardId shardId, GetResult originalIndex, Index currentIndex, IndexResult result) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override public void logDocumentDeleted(ShardId shardId, Delete delete, DeleteResult result) { - //noop, intentionally left empty + // noop, intentionally left empty } @Override diff --git a/src/main/java/org/opensearch/security/auditlog/config/AuditConfig.java b/src/main/java/org/opensearch/security/auditlog/config/AuditConfig.java index 1152abe89e..f6f9a42e87 100644 --- a/src/main/java/org/opensearch/security/auditlog/config/AuditConfig.java +++ b/src/main/java/org/opensearch/security/auditlog/config/AuditConfig.java @@ -108,10 +108,7 @@ public ComplianceConfig getCompliance() { } @VisibleForTesting - public AuditConfig( - final boolean auditLogEnabled, - final Filter filter, - final ComplianceConfig compliance) { + public AuditConfig(final boolean auditLogEnabled, final Filter filter, final ComplianceConfig compliance) { this.auditLogEnabled = auditLogEnabled; this.filter = filter != null ? filter : Filter.DEFAULT; this.compliance = compliance != null ? compliance : ComplianceConfig.DEFAULT; @@ -147,16 +144,18 @@ public static class Filter { private final Set disabledTransportCategories; @VisibleForTesting - Filter(final boolean isRestApiAuditEnabled, - final boolean isTransportApiAuditEnabled, - final boolean resolveBulkRequests, - final boolean logRequestBody, - final boolean resolveIndices, - final boolean excludeSensitiveHeaders, - final Set ignoredAuditUsers, - final Set ignoredAuditRequests, - final Set disabledRestCategories, - final Set disabledTransportCategories) { + Filter( + final boolean isRestApiAuditEnabled, + final boolean isTransportApiAuditEnabled, + final boolean resolveBulkRequests, + final boolean logRequestBody, + final boolean resolveIndices, + final boolean excludeSensitiveHeaders, + final Set ignoredAuditUsers, + final Set ignoredAuditRequests, + final Set disabledRestCategories, + final Set disabledTransportCategories + ) { this.isRestApiAuditEnabled = isRestApiAuditEnabled; this.isTransportApiAuditEnabled = isTransportApiAuditEnabled; this.resolveBulkRequests = resolveBulkRequests; @@ -179,22 +178,29 @@ public enum FilterEntries { RESOLVE_INDICES("resolve_indices", ConfigConstants.OPENDISTRO_SECURITY_AUDIT_RESOLVE_INDICES), EXCLUDE_SENSITIVE_HEADERS("exclude_sensitive_headers", ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXCLUDE_SENSITIVE_HEADERS), DISABLE_REST_CATEGORIES("disabled_rest_categories", ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES), - DISABLE_TRANSPORT_CATEGORIES("disabled_transport_categories", ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_TRANSPORT_CATEGORIES), + DISABLE_TRANSPORT_CATEGORIES( + "disabled_transport_categories", + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_TRANSPORT_CATEGORIES + ), IGNORE_USERS("ignore_users", ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_USERS), IGNORE_REQUESTS("ignore_requests", ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS); private final String key; private final String legacyKeyWithNamespace; + FilterEntries(final String entryKey, final String legacyKeyWithNamespace) { this.key = entryKey; this.legacyKeyWithNamespace = legacyKeyWithNamespace; } + public String getKey() { return this.key; } + public String getKeyWithNamespace() { - return SECURITY_AUDIT_CONFIG_DEFAULT + "."+ this.key; + return SECURITY_AUDIT_CONFIG_DEFAULT + "." + this.key; } + public String getLegacyKeyWithNamespace() { return this.legacyKeyWithNamespace; } @@ -204,7 +210,14 @@ public String getLegacyKeyWithNamespace() { @VisibleForTesting public static Filter from(Map properties) throws JsonProcessingException { if (!FIELDS.containsAll(properties.keySet())) { - throw new UnrecognizedPropertyException(null, "Unrecognized field(s) present in the input data for audit filter config", null, Filter.class, null, null); + throw new UnrecognizedPropertyException( + null, + "Unrecognized field(s) present in the input data for audit filter config", + null, + Filter.class, + null, + null + ); } final boolean isRestApiAuditEnabled = getOrDefault(properties, FilterEntries.ENABLE_REST.getKey(), true); @@ -213,22 +226,39 @@ public static Filter from(Map properties) throws JsonProcessingE final boolean logRequestBody = getOrDefault(properties, FilterEntries.LOG_REQUEST_BODY.getKey(), true); final boolean resolveIndices = getOrDefault(properties, FilterEntries.RESOLVE_INDICES.getKey(), true); final boolean excludeSensitiveHeaders = getOrDefault(properties, FilterEntries.EXCLUDE_SENSITIVE_HEADERS.getKey(), true); - final Set disabledRestCategories = AuditCategory.parse(getOrDefault(properties, FilterEntries.DISABLE_REST_CATEGORIES.getKey(), ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT)); - final Set disabledTransportCategories = AuditCategory.parse(getOrDefault(properties, FilterEntries.DISABLE_TRANSPORT_CATEGORIES.getKey(), ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT)); - final Set ignoredAuditUsers = ImmutableSet.copyOf(getOrDefault(properties, FilterEntries.IGNORE_USERS.getKey(), DEFAULT_IGNORED_USERS)); - final Set ignoreAuditRequests = ImmutableSet.copyOf(getOrDefault(properties, FilterEntries.IGNORE_REQUESTS.getKey(), Collections.emptyList())); + final Set disabledRestCategories = AuditCategory.parse( + getOrDefault( + properties, + FilterEntries.DISABLE_REST_CATEGORIES.getKey(), + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT + ) + ); + final Set disabledTransportCategories = AuditCategory.parse( + getOrDefault( + properties, + FilterEntries.DISABLE_TRANSPORT_CATEGORIES.getKey(), + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT + ) + ); + final Set ignoredAuditUsers = ImmutableSet.copyOf( + getOrDefault(properties, FilterEntries.IGNORE_USERS.getKey(), DEFAULT_IGNORED_USERS) + ); + final Set ignoreAuditRequests = ImmutableSet.copyOf( + getOrDefault(properties, FilterEntries.IGNORE_REQUESTS.getKey(), Collections.emptyList()) + ); return new Filter( - isRestApiAuditEnabled, - isTransportAuditEnabled, - resolveBulkRequests, - logRequestBody, - resolveIndices, - excludeSensitiveHeaders, - ignoredAuditUsers, - ignoreAuditRequests, - disabledRestCategories, - disabledTransportCategories); + isRestApiAuditEnabled, + isTransportAuditEnabled, + resolveBulkRequests, + logRequestBody, + resolveIndices, + excludeSensitiveHeaders, + ignoredAuditUsers, + ignoreAuditRequests, + disabledRestCategories, + disabledTransportCategories + ); } @@ -244,34 +274,52 @@ public static Filter from(Settings settings) { final boolean logRequestBody = fromSettingBoolean(settings, FilterEntries.LOG_REQUEST_BODY, true); final boolean resolveIndices = fromSettingBoolean(settings, FilterEntries.RESOLVE_INDICES, true); final boolean excludeSensitiveHeaders = fromSettingBoolean(settings, FilterEntries.EXCLUDE_SENSITIVE_HEADERS, true); - final Set disabledRestCategories = AuditCategory.parse(fromSettingStringSet(settings, FilterEntries.DISABLE_REST_CATEGORIES, ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT)); - final Set disabledTransportCategories = AuditCategory.parse(fromSettingStringSet(settings, FilterEntries.DISABLE_TRANSPORT_CATEGORIES, ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT)); + final Set disabledRestCategories = AuditCategory.parse( + fromSettingStringSet( + settings, + FilterEntries.DISABLE_REST_CATEGORIES, + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT + ) + ); + final Set disabledTransportCategories = AuditCategory.parse( + fromSettingStringSet( + settings, + FilterEntries.DISABLE_TRANSPORT_CATEGORIES, + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_DISABLED_CATEGORIES_DEFAULT + ) + ); final Set ignoredAuditUsers = fromSettingStringSet(settings, FilterEntries.IGNORE_USERS, DEFAULT_IGNORED_USERS); final Set ignoreAuditRequests = fromSettingStringSet(settings, FilterEntries.IGNORE_REQUESTS, Collections.emptyList()); - return new Filter(isRestApiAuditEnabled, - isTransportAuditEnabled, - resolveBulkRequests, - logRequestBody, - resolveIndices, - excludeSensitiveHeaders, - ignoredAuditUsers, - ignoreAuditRequests, - disabledRestCategories, - disabledTransportCategories); + return new Filter( + isRestApiAuditEnabled, + isTransportAuditEnabled, + resolveBulkRequests, + logRequestBody, + resolveIndices, + excludeSensitiveHeaders, + ignoredAuditUsers, + ignoreAuditRequests, + disabledRestCategories, + disabledTransportCategories + ); } static boolean fromSettingBoolean(final Settings settings, FilterEntries filterEntry, final boolean defaultValue) { - return settings.getAsBoolean(filterEntry.getKeyWithNamespace(), settings.getAsBoolean(filterEntry.getLegacyKeyWithNamespace(), defaultValue)); + return settings.getAsBoolean( + filterEntry.getKeyWithNamespace(), + settings.getAsBoolean(filterEntry.getLegacyKeyWithNamespace(), defaultValue) + ); } static Set fromSettingStringSet(final Settings settings, FilterEntries filterEntry, final List defaultValue) { final String defaultDetectorValue = "__DEFAULT_DETECTION__"; final Set stringSetOfKey = ConfigConstants.getSettingAsSet( - settings, - filterEntry.getKeyWithNamespace(), - ImmutableList.of(defaultDetectorValue), - true); + settings, + filterEntry.getKeyWithNamespace(), + ImmutableList.of(defaultDetectorValue), + true + ); final boolean foundDefault = stringSetOfKey.stream().anyMatch(defaultDetectorValue::equals); if (!foundDefault) { @@ -279,11 +327,7 @@ static Set fromSettingStringSet(final Settings settings, FilterEntries f } // Fallback to the legacy keyname - return ConfigConstants.getSettingAsSet( - settings, - filterEntry.getLegacyKeyWithNamespace(), - defaultValue, - true); + return ConfigConstants.getSettingAsSet(settings, filterEntry.getLegacyKeyWithNamespace(), defaultValue, true); } /** @@ -400,18 +444,28 @@ public void log(Logger logger) { @Override public String toString() { - return "Filter{" + - "isRestApiAuditEnabled=" + isRestApiAuditEnabled + - ", disabledRestCategories=" + disabledRestCategories + - ", isTransportApiAuditEnabled=" + isTransportApiAuditEnabled + - ", disabledTransportCategories=" + disabledTransportCategories + - ", resolveBulkRequests=" + resolveBulkRequests + - ", logRequestBody=" + logRequestBody + - ", resolveIndices=" + resolveIndices + - ", excludeSensitiveHeaders=" + excludeSensitiveHeaders + - ", ignoredAuditUsers=" + ignoredAuditUsersMatcher + - ", ignoreAuditRequests=" + ignoredAuditRequestsMatcher + - '}'; + return "Filter{" + + "isRestApiAuditEnabled=" + + isRestApiAuditEnabled + + ", disabledRestCategories=" + + disabledRestCategories + + ", isTransportApiAuditEnabled=" + + isTransportApiAuditEnabled + + ", disabledTransportCategories=" + + disabledTransportCategories + + ", resolveBulkRequests=" + + resolveBulkRequests + + ", logRequestBody=" + + logRequestBody + + ", resolveIndices=" + + resolveIndices + + ", excludeSensitiveHeaders=" + + excludeSensitiveHeaders + + ", ignoredAuditUsers=" + + ignoredAuditUsersMatcher + + ", ignoreAuditRequests=" + + ignoredAuditRequestsMatcher + + '}'; } } @@ -419,39 +473,36 @@ public String toString() { * List of keys that are deprecated */ public static final List DEPRECATED_KEYS = ImmutableList.of( - ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ENABLE_REST, - ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES, - ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ENABLE_TRANSPORT, - ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_TRANSPORT_CATEGORIES, - ConfigConstants.OPENDISTRO_SECURITY_AUDIT_LOG_REQUEST_BODY, - ConfigConstants.OPENDISTRO_SECURITY_AUDIT_RESOLVE_INDICES, - ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXCLUDE_SENSITIVE_HEADERS, - ConfigConstants.OPENDISTRO_SECURITY_AUDIT_RESOLVE_BULK_REQUESTS, - ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_USERS, - ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS, - ConfigConstants.SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED, - ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED, - ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_METADATA_ONLY, - ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_IGNORE_USERS, - ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS, - ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_METADATA_ONLY, - ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_LOG_DIFFS, - ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_IGNORE_USERS, - ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ENABLE_REST, + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_REST_CATEGORIES, + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_ENABLE_TRANSPORT, + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_CONFIG_DISABLED_TRANSPORT_CATEGORIES, + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_LOG_REQUEST_BODY, + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_RESOLVE_INDICES, + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_EXCLUDE_SENSITIVE_HEADERS, + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_RESOLVE_BULK_REQUESTS, + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_USERS, + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_IGNORE_REQUESTS, + ConfigConstants.SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED, + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED, + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_METADATA_ONLY, + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_IGNORE_USERS, + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS, + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_METADATA_ONLY, + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_LOG_DIFFS, + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_IGNORE_USERS, + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES ); public static Set getDeprecatedKeys(final Settings settings) { - return AuditConfig.DEPRECATED_KEYS - .stream() - .filter(settings::hasValue) - .collect(Collectors.toSet()); + return AuditConfig.DEPRECATED_KEYS.stream().filter(settings::hasValue).collect(Collectors.toSet()); } public static final Set FIELD_PATHS = Sets.union( - Utils.generateFieldResourcePaths(AuditConfig.FIELDS, "/"), - Sets.union( - Utils.generateFieldResourcePaths(Filter.FIELDS, "/audit/"), - Utils.generateFieldResourcePaths(ComplianceConfig.FIELDS, "/compliance/") - ) + Utils.generateFieldResourcePaths(AuditConfig.FIELDS, "/"), + Sets.union( + Utils.generateFieldResourcePaths(Filter.FIELDS, "/audit/"), + Utils.generateFieldResourcePaths(ComplianceConfig.FIELDS, "/compliance/") + ) ); } diff --git a/src/main/java/org/opensearch/security/auditlog/config/ThreadPoolConfig.java b/src/main/java/org/opensearch/security/auditlog/config/ThreadPoolConfig.java index a0ef7937ff..a8c44e0cee 100644 --- a/src/main/java/org/opensearch/security/auditlog/config/ThreadPoolConfig.java +++ b/src/main/java/org/opensearch/security/auditlog/config/ThreadPoolConfig.java @@ -27,7 +27,9 @@ public ThreadPoolConfig(int threadPoolSize, int threadPoolMaxQueueLen) { } if (threadPoolMaxQueueLen <= 0) { - throw new IllegalArgumentException("Incorrect thread pool queue length: " + threadPoolMaxQueueLen + " configured for audit logging."); + throw new IllegalArgumentException( + "Incorrect thread pool queue length: " + threadPoolMaxQueueLen + " configured for audit logging." + ); } this.threadPoolSize = threadPoolSize; @@ -44,7 +46,10 @@ public int getThreadPoolMaxQueueLen() { public static ThreadPoolConfig getConfig(Settings settings) { int threadPoolSize = settings.getAsInt(ConfigConstants.SECURITY_AUDIT_THREADPOOL_SIZE, DEFAULT_THREAD_POOL_SIZE); - int threadPoolMaxQueueLen = settings.getAsInt(ConfigConstants.SECURITY_AUDIT_THREADPOOL_MAX_QUEUE_LEN, DEFAULT_THREAD_POOL_MAX_QUEUE_LEN); + int threadPoolMaxQueueLen = settings.getAsInt( + ConfigConstants.SECURITY_AUDIT_THREADPOOL_MAX_QUEUE_LEN, + DEFAULT_THREAD_POOL_MAX_QUEUE_LEN + ); return new ThreadPoolConfig(threadPoolSize, threadPoolMaxQueueLen); } diff --git a/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java b/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java index e4aa062641..e14d5b17a9 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AbstractAuditLog.java @@ -75,7 +75,6 @@ import static org.opensearch.core.xcontent.DeprecationHandler.THROW_UNSUPPORTED_OPERATION; - public abstract class AbstractAuditLog implements AuditLog { protected final Logger log = LogManager.getLogger(this.getClass()); @@ -100,13 +99,22 @@ public abstract class AbstractAuditLog implements AuditLog { writeClasses.add(DeleteRequest.class.getSimpleName()); } - protected AbstractAuditLog(Settings settings, final ThreadPool threadPool, final IndexNameExpressionResolver resolver, final ClusterService clusterService, final Environment environment) { + protected AbstractAuditLog( + Settings settings, + final ThreadPool threadPool, + final IndexNameExpressionResolver resolver, + final ClusterService clusterService, + final Environment environment + ) { super(); this.threadPool = threadPool; this.settings = settings; this.resolver = resolver; this.clusterService = clusterService; - this.securityIndex = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + this.securityIndex = settings.get( + ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + ); this.environment = environment; } @@ -133,7 +141,7 @@ public ComplianceConfig getComplianceConfig() { @Override public void logFailedLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request) { - if(!checkRestFilter(AuditCategory.FAILED_LOGIN, effectiveUser, request)) { + if (!checkRestFilter(AuditCategory.FAILED_LOGIN, effectiveUser, request)) { return; } @@ -151,7 +159,7 @@ public void logFailedLogin(String effectiveUser, boolean securityadmin, String i @Override public void logSucceededLogin(String effectiveUser, boolean securityadmin, String initiatingUser, RestRequest request) { - if(!checkRestFilter(AuditCategory.AUTHENTICATED, effectiveUser, request)) { + if (!checkRestFilter(AuditCategory.AUTHENTICATED, effectiveUser, request)) { return; } @@ -167,7 +175,7 @@ public void logSucceededLogin(String effectiveUser, boolean securityadmin, Strin @Override public void logMissingPrivileges(String privilege, String effectiveUser, RestRequest request) { - if(!checkRestFilter(AuditCategory.MISSING_PRIVILEGES, effectiveUser, request)) { + if (!checkRestFilter(AuditCategory.MISSING_PRIVILEGES, effectiveUser, request)) { return; } @@ -181,7 +189,7 @@ public void logMissingPrivileges(String privilege, String effectiveUser, RestReq @Override public void logGrantedPrivileges(String effectiveUser, RestRequest request) { - if(!checkRestFilter(AuditCategory.GRANTED_PRIVILEGES, effectiveUser, request)) { + if (!checkRestFilter(AuditCategory.GRANTED_PRIVILEGES, effectiveUser, request)) { return; } @@ -196,14 +204,35 @@ public void logGrantedPrivileges(String effectiveUser, RestRequest request) { public void logMissingPrivileges(String privilege, TransportRequest request, Task task) { final String action = null; - if(!checkTransportFilter(AuditCategory.MISSING_PRIVILEGES, privilege, getUser(), request)) { + if (!checkTransportFilter(AuditCategory.MISSING_PRIVILEGES, privilege, getUser(), request)) { return; } final TransportAddress remoteAddress = getRemoteAddress(); - final List msgs = RequestResolver.resolve(AuditCategory.MISSING_PRIVILEGES, getOrigin(), action, privilege, getUser(), null, null, remoteAddress, request, getThreadContextHeaders(), task, resolver, clusterService, settings, auditConfigFilter.shouldLogRequestBody(), auditConfigFilter.shouldResolveIndices(), auditConfigFilter.shouldResolveBulkRequests(), securityIndex, auditConfigFilter.shouldExcludeSensitiveHeaders(), null); - - for(AuditMessage msg: msgs) { + final List msgs = RequestResolver.resolve( + AuditCategory.MISSING_PRIVILEGES, + getOrigin(), + action, + privilege, + getUser(), + null, + null, + remoteAddress, + request, + getThreadContextHeaders(), + task, + resolver, + clusterService, + settings, + auditConfigFilter.shouldLogRequestBody(), + auditConfigFilter.shouldResolveIndices(), + auditConfigFilter.shouldResolveBulkRequests(), + securityIndex, + auditConfigFilter.shouldExcludeSensitiveHeaders(), + null + ); + + for (AuditMessage msg : msgs) { save(msg); } } @@ -212,21 +241,42 @@ public void logMissingPrivileges(String privilege, TransportRequest request, Tas public void logGrantedPrivileges(String privilege, TransportRequest request, Task task) { final String action = null; - if(!checkTransportFilter(AuditCategory.GRANTED_PRIVILEGES, privilege, getUser(), request)) { + if (!checkTransportFilter(AuditCategory.GRANTED_PRIVILEGES, privilege, getUser(), request)) { return; } final TransportAddress remoteAddress = getRemoteAddress(); - final List msgs = RequestResolver.resolve(AuditCategory.GRANTED_PRIVILEGES, getOrigin(), action, privilege, getUser(), null, null, remoteAddress, request, getThreadContextHeaders(), task, resolver, clusterService, settings, auditConfigFilter.shouldLogRequestBody(), auditConfigFilter.shouldResolveIndices(), auditConfigFilter.shouldResolveBulkRequests(), securityIndex, auditConfigFilter.shouldExcludeSensitiveHeaders(), null); - - for(AuditMessage msg: msgs) { + final List msgs = RequestResolver.resolve( + AuditCategory.GRANTED_PRIVILEGES, + getOrigin(), + action, + privilege, + getUser(), + null, + null, + remoteAddress, + request, + getThreadContextHeaders(), + task, + resolver, + clusterService, + settings, + auditConfigFilter.shouldLogRequestBody(), + auditConfigFilter.shouldResolveIndices(), + auditConfigFilter.shouldResolveBulkRequests(), + securityIndex, + auditConfigFilter.shouldExcludeSensitiveHeaders(), + null + ); + + for (AuditMessage msg : msgs) { save(msg); } } @Override public void logIndexEvent(String privilege, TransportRequest request, Task task) { - if(!checkTransportFilter(AuditCategory.INDEX_EVENT, privilege, getUser(), request)) { + if (!checkTransportFilter(AuditCategory.INDEX_EVENT, privilege, getUser(), request)) { return; } // log only cluster admin action @@ -234,7 +284,28 @@ public void logIndexEvent(String privilege, TransportRequest request, Task task) return; } final TransportAddress remoteAddress = getRemoteAddress(); - final List msgs = RequestResolver.resolve(AuditCategory.INDEX_EVENT, getOrigin(), null, privilege, getUser(), null, null, remoteAddress, request, getThreadContextHeaders(), task, resolver, clusterService, settings, auditConfigFilter.shouldLogRequestBody(), auditConfigFilter.shouldResolveIndices(), auditConfigFilter.shouldResolveBulkRequests(), securityIndex, auditConfigFilter.shouldExcludeSensitiveHeaders(), null); + final List msgs = RequestResolver.resolve( + AuditCategory.INDEX_EVENT, + getOrigin(), + null, + privilege, + getUser(), + null, + null, + remoteAddress, + request, + getThreadContextHeaders(), + task, + resolver, + clusterService, + settings, + auditConfigFilter.shouldLogRequestBody(), + auditConfigFilter.shouldResolveIndices(), + auditConfigFilter.shouldResolveBulkRequests(), + securityIndex, + auditConfigFilter.shouldExcludeSensitiveHeaders(), + null + ); msgs.forEach(this::save); } @@ -242,14 +313,35 @@ public void logIndexEvent(String privilege, TransportRequest request, Task task) @Override public void logBadHeaders(TransportRequest request, String action, Task task) { - if(!checkTransportFilter(AuditCategory.BAD_HEADERS, action, getUser(), request)) { + if (!checkTransportFilter(AuditCategory.BAD_HEADERS, action, getUser(), request)) { return; } final TransportAddress remoteAddress = getRemoteAddress(); - final List msgs = RequestResolver.resolve(AuditCategory.BAD_HEADERS, getOrigin(), action, null, getUser(), null, null, remoteAddress, request, getThreadContextHeaders(), task, resolver, clusterService, settings, auditConfigFilter.shouldLogRequestBody(), auditConfigFilter.shouldResolveIndices(), auditConfigFilter.shouldResolveBulkRequests(), securityIndex, auditConfigFilter.shouldExcludeSensitiveHeaders(), null); - - for(AuditMessage msg: msgs) { + final List msgs = RequestResolver.resolve( + AuditCategory.BAD_HEADERS, + getOrigin(), + action, + null, + getUser(), + null, + null, + remoteAddress, + request, + getThreadContextHeaders(), + task, + resolver, + clusterService, + settings, + auditConfigFilter.shouldLogRequestBody(), + auditConfigFilter.shouldResolveIndices(), + auditConfigFilter.shouldResolveBulkRequests(), + securityIndex, + auditConfigFilter.shouldExcludeSensitiveHeaders(), + null + ); + + for (AuditMessage msg : msgs) { save(msg); } } @@ -257,7 +349,7 @@ public void logBadHeaders(TransportRequest request, String action, Task task) { @Override public void logBadHeaders(RestRequest request) { - if(!checkRestFilter(AuditCategory.BAD_HEADERS, getUser(), request)) { + if (!checkRestFilter(AuditCategory.BAD_HEADERS, getUser(), request)) { return; } @@ -273,14 +365,35 @@ public void logBadHeaders(RestRequest request) { @Override public void logSecurityIndexAttempt(TransportRequest request, String action, Task task) { - if(!checkTransportFilter(AuditCategory.OPENDISTRO_SECURITY_INDEX_ATTEMPT, action, getUser(), request)) { + if (!checkTransportFilter(AuditCategory.OPENDISTRO_SECURITY_INDEX_ATTEMPT, action, getUser(), request)) { return; } final TransportAddress remoteAddress = getRemoteAddress(); - final List msgs = RequestResolver.resolve(AuditCategory.OPENDISTRO_SECURITY_INDEX_ATTEMPT, getOrigin(), action, null, getUser(), false, null, remoteAddress, request, getThreadContextHeaders(), task, resolver, clusterService, settings, auditConfigFilter.shouldLogRequestBody(), auditConfigFilter.shouldResolveIndices(), auditConfigFilter.shouldResolveBulkRequests(), securityIndex, auditConfigFilter.shouldExcludeSensitiveHeaders(), null); - - for(AuditMessage msg: msgs) { + final List msgs = RequestResolver.resolve( + AuditCategory.OPENDISTRO_SECURITY_INDEX_ATTEMPT, + getOrigin(), + action, + null, + getUser(), + false, + null, + remoteAddress, + request, + getThreadContextHeaders(), + task, + resolver, + clusterService, + settings, + auditConfigFilter.shouldLogRequestBody(), + auditConfigFilter.shouldResolveIndices(), + auditConfigFilter.shouldResolveBulkRequests(), + securityIndex, + auditConfigFilter.shouldExcludeSensitiveHeaders(), + null + ); + + for (AuditMessage msg : msgs) { save(msg); } } @@ -288,16 +401,36 @@ public void logSecurityIndexAttempt(TransportRequest request, String action, Tas @Override public void logSSLException(TransportRequest request, Throwable t, String action, Task task) { - if(!checkTransportFilter(AuditCategory.SSL_EXCEPTION, action, getUser(), request)) { + if (!checkTransportFilter(AuditCategory.SSL_EXCEPTION, action, getUser(), request)) { return; } final TransportAddress remoteAddress = getRemoteAddress(); - final List msgs = RequestResolver.resolve(AuditCategory.SSL_EXCEPTION, Origin.TRANSPORT, action, null, getUser(), false, null, remoteAddress, request, - getThreadContextHeaders(), task, resolver, clusterService, settings, auditConfigFilter.shouldLogRequestBody(), auditConfigFilter.shouldResolveIndices(), auditConfigFilter.shouldResolveBulkRequests(), securityIndex, auditConfigFilter.shouldExcludeSensitiveHeaders(), t); - - for(AuditMessage msg: msgs) { + final List msgs = RequestResolver.resolve( + AuditCategory.SSL_EXCEPTION, + Origin.TRANSPORT, + action, + null, + getUser(), + false, + null, + remoteAddress, + request, + getThreadContextHeaders(), + task, + resolver, + clusterService, + settings, + auditConfigFilter.shouldLogRequestBody(), + auditConfigFilter.shouldResolveIndices(), + auditConfigFilter.shouldResolveBulkRequests(), + securityIndex, + auditConfigFilter.shouldExcludeSensitiveHeaders(), + t + ); + + for (AuditMessage msg : msgs) { save(msg); } } @@ -305,7 +438,7 @@ public void logSSLException(TransportRequest request, Throwable t, String action @Override public void logSSLException(RestRequest request, Throwable t) { - if(!checkRestFilter(AuditCategory.SSL_EXCEPTION, getUser(), request)) { + if (!checkRestFilter(AuditCategory.SSL_EXCEPTION, getUser(), request)) { return; } @@ -322,36 +455,39 @@ public void logSSLException(RestRequest request, Throwable t) { @Override public void logDocumentRead(String index, String id, ShardId shardId, Map fieldNameValues) { final ComplianceConfig complianceConfig = getComplianceConfig(); - if(complianceConfig == null || !complianceConfig.readHistoryEnabledForIndex(index)) { + if (complianceConfig == null || !complianceConfig.readHistoryEnabledForIndex(index)) { return; } - final String initiatingRequestClass = threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER); + final String initiatingRequestClass = threadPool.getThreadContext() + .getHeader(ConfigConstants.OPENDISTRO_SECURITY_INITIAL_ACTION_CLASS_HEADER); - if(initiatingRequestClass != null && writeClasses.contains(initiatingRequestClass)) { + if (initiatingRequestClass != null && writeClasses.contains(initiatingRequestClass)) { return; } - AuditCategory category = securityIndex.equals(index)? AuditCategory.COMPLIANCE_INTERNAL_CONFIG_READ: AuditCategory.COMPLIANCE_DOC_READ; + AuditCategory category = securityIndex.equals(index) + ? AuditCategory.COMPLIANCE_INTERNAL_CONFIG_READ + : AuditCategory.COMPLIANCE_DOC_READ; String effectiveUser = getUser(); - if(!checkComplianceFilter(category, effectiveUser, getOrigin(), complianceConfig)) { + if (!checkComplianceFilter(category, effectiveUser, getOrigin(), complianceConfig)) { return; } - if(fieldNameValues != null && !fieldNameValues.isEmpty()) { + if (fieldNameValues != null && !fieldNameValues.isEmpty()) { AuditMessage msg = new AuditMessage(category, clusterService, getOrigin(), null); TransportAddress remoteAddress = getRemoteAddress(); msg.addRemoteAddress(remoteAddress); msg.addEffectiveUser(effectiveUser); - msg.addIndices(new String[]{index}); - msg.addResolvedIndices(new String[]{index}); + msg.addIndices(new String[] { index }); + msg.addResolvedIndices(new String[] { index }); msg.addShardId(shardId); - //msg.addIsAdminDn(securityadmin); + // msg.addIsAdminDn(securityadmin); msg.addId(id); try { - if(complianceConfig.shouldLogReadMetadataOnly()) { + if (complianceConfig.shouldLogReadMetadataOnly()) { try { XContentBuilder builder = XContentBuilder.builder(JsonXContent.jsonXContent); builder.startObject(); @@ -363,10 +499,19 @@ public void logDocumentRead(String index, String id, ShardId shardId, Map map = fieldNameValues.entrySet().stream() - .collect(Collectors.toMap(entry -> "id", entry -> new String(BaseEncoding.base64().decode(((Entry) entry).getValue()), StandardCharsets.UTF_8))); + Map map = fieldNameValues.entrySet() + .stream() + .collect( + Collectors.toMap( + entry -> "id", + entry -> new String( + BaseEncoding.base64().decode(((Entry) entry).getValue()), + StandardCharsets.UTF_8 + ) + ) + ); msg.addSecurityConfigMapToRequestBody(Utils.convertJsonToxToStructuredMap(map.get("id")), id); } catch (Exception e) { msg.addSecurityConfigMapToRequestBody(fieldNameValues, id); @@ -376,7 +521,7 @@ public void logDocumentRead(String index, String id, ShardId shardId, Map(XContentType.JSON, currentIndex.source()), id); - } + if (!complianceConfig.shouldLogWriteMetadataOnly()) { + if (securityIndex.equals(shardId.getIndexName())) { + // current source, normally not null or empty + try ( + XContentParser parser = XContentHelper.createParser( + NamedXContentRegistry.EMPTY, + THROW_UNSUPPORTED_OPERATION, + currentIndex.source(), + XContentType.JSON + ) + ) { + Object base64 = parser.map().values().iterator().next(); + if (base64 instanceof String) { + msg.addSecurityConfigContentToRequestBody( + new String(BaseEncoding.base64().decode((String) base64), StandardCharsets.UTF_8), + id + ); + } else { + msg.addSecurityConfigTupleToRequestBody( + new Tuple(XContentType.JSON, currentIndex.source()), + id + ); + } } catch (Exception e) { log.error(e.toString()); } - //if we want to have msg.ComplianceWritePreviousSource we need to do the same as above + // if we want to have msg.ComplianceWritePreviousSource we need to do the same as above } else { - //previous source, can be null if document is a new one - //msg.ComplianceWritePreviousSource(new Tuple(XContentType.JSON, originalResult.internalSourceRef())); + // previous source, can be null if document is a new one + // msg.ComplianceWritePreviousSource(new Tuple(XContentType.JSON, + // originalResult.internalSourceRef())); - //current source, normally not null or empty + // current source, normally not null or empty msg.addTupleToRequestBody(new Tuple(XContentType.JSON, currentIndex.source())); } } - save(msg); } @@ -488,7 +670,9 @@ public void logDocumentDeleted(ShardId shardId, Delete delete, DeleteResult resu String effectiveUser = getUser(); final ComplianceConfig complianceConfig = getComplianceConfig(); - if (complianceConfig == null || !complianceConfig.isEnabled() || !checkComplianceFilter(AuditCategory.COMPLIANCE_DOC_WRITE, effectiveUser, getOrigin(), complianceConfig)) { + if (complianceConfig == null + || !complianceConfig.isEnabled() + || !checkComplianceFilter(AuditCategory.COMPLIANCE_DOC_WRITE, effectiveUser, getOrigin(), complianceConfig)) { return; } @@ -496,8 +680,8 @@ public void logDocumentDeleted(ShardId shardId, Delete delete, DeleteResult resu TransportAddress remoteAddress = getRemoteAddress(); msg.addRemoteAddress(remoteAddress); msg.addEffectiveUser(effectiveUser); - msg.addIndices(new String[]{shardId.getIndexName()}); - msg.addResolvedIndices(new String[]{shardId.getIndexName()}); + msg.addIndices(new String[] { shardId.getIndexName() }); + msg.addResolvedIndices(new String[] { shardId.getIndexName() }); msg.addId(delete.id()); msg.addShardId(shardId); msg.addComplianceDocVersion(result.getVersion()); @@ -509,7 +693,11 @@ public void logDocumentDeleted(ShardId shardId, Delete delete, DeleteResult resu protected void logExternalConfig() { final ComplianceConfig complianceConfig = getComplianceConfig(); - if (complianceConfig == null || !complianceConfig.isEnabled() || !complianceConfig.shouldLogExternalConfig() || !checkComplianceFilter(AuditCategory.COMPLIANCE_EXTERNAL_CONFIG, null, getOrigin(), complianceConfig) || externalConfigLogged.getAndSet(true)) { + if (complianceConfig == null + || !complianceConfig.isEnabled() + || !complianceConfig.shouldLogExternalConfig() + || !checkComplianceFilter(AuditCategory.COMPLIANCE_EXTERNAL_CONFIG, null, getOrigin(), complianceConfig) + || externalConfigLogged.getAndSet(true)) { return; } @@ -536,7 +724,7 @@ public Map run() { } }); - final String sha256 = DigestUtils.sha256Hex(configAsMap.toString()+envAsMap.toString()+propsAsMap.toString()); + final String sha256 = DigestUtils.sha256Hex(configAsMap.toString() + envAsMap.toString() + propsAsMap.toString()); AuditMessage msg = new AuditMessage(AuditCategory.COMPLIANCE_EXTERNAL_CONFIG, clusterService, null, null); try (XContentBuilder builder = XContentBuilder.builder(XContentType.JSON.xContent())) { @@ -551,50 +739,54 @@ public Map run() { builder.close(); msg.addUnescapedJsonToRequestBody(Strings.toString(builder)); } catch (Exception e) { - log.error("Unable to build message",e); + log.error("Unable to build message", e); } Map paths = new HashMap(); - for(String key: settings.keySet()) { - if(key.startsWith("opendistro_security") && - (key.contains("filepath") || key.contains("file_path"))) { + for (String key : settings.keySet()) { + if (key.startsWith("opendistro_security") && (key.contains("filepath") || key.contains("file_path"))) { String value = settings.get(key); - if(value != null && !value.isEmpty()) { - Path path = value.startsWith("/")?Paths.get(value):environment.configDir().resolve(value); + if (value != null && !value.isEmpty()) { + Path path = value.startsWith("/") ? Paths.get(value) : environment.configDir().resolve(value); paths.put(key, path); } } } msg.addFileInfos(paths); - save(msg); } private Origin getOrigin() { String origin = (String) threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN); - if(origin == null && threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADER) != null) { + if (origin == null && threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADER) != null) { origin = threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN_HEADER); } - return origin == null?null:Origin.valueOf(origin); + return origin == null ? null : Origin.valueOf(origin); } private TransportAddress getRemoteAddress() { TransportAddress address = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - if(address == null && threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER) != null) { - address = new TransportAddress((InetSocketAddress) Base64Helper.deserializeObject(threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER))); + if (address == null && threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER) != null) { + address = new TransportAddress( + (InetSocketAddress) Base64Helper.deserializeObject( + threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS_HEADER) + ) + ); } return address; } private String getUser() { User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - if(user == null && threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER) != null) { - user = (User) Base64Helper.deserializeObject(threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER)); + if (user == null && threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER) != null) { + user = (User) Base64Helper.deserializeObject( + threadPool.getThreadContext().getHeader(ConfigConstants.OPENDISTRO_SECURITY_USER_HEADER) + ); } - return user==null?null:user.getName(); + return user == null ? null : user.getName(); } private Map getThreadContextHeaders() { @@ -605,15 +797,20 @@ private Map getThreadContextHeaders() { boolean checkTransportFilter(final AuditCategory category, final String action, final String effectiveUser, TransportRequest request) { final boolean isTraceEnabled = log.isTraceEnabled(); if (isTraceEnabled) { - log.trace("Check category:{}, action:{}, effectiveUser:{}, request:{}", category, action, effectiveUser, request==null?null:request.getClass().getSimpleName()); + log.trace( + "Check category:{}, action:{}, effectiveUser:{}, request:{}", + category, + action, + effectiveUser, + request == null ? null : request.getClass().getSimpleName() + ); } - if (!auditConfigFilter.isTransportApiAuditEnabled()) { return false; } - //skip internals + // skip internals if (action != null && action.startsWith("internal:")) { return false; } @@ -627,10 +824,12 @@ boolean checkTransportFilter(final AuditCategory category, final String action, return false; } - if (request != null && (auditConfigFilter.isRequestAuditDisabled(action) || auditConfigFilter.isRequestAuditDisabled(request.getClass().getSimpleName()))) { + if (request != null + && (auditConfigFilter.isRequestAuditDisabled(action) + || auditConfigFilter.isRequestAuditDisabled(request.getClass().getSimpleName()))) { if (isTraceEnabled) { - log.trace("Skipped audit log message because request {} is ignored", action+"#"+request.getClass().getSimpleName()); + log.trace("Skipped audit log message because request {} is ignored", action + "#" + request.getClass().getSimpleName()); } return false; @@ -645,29 +844,33 @@ boolean checkTransportFilter(final AuditCategory category, final String action, return false; } - - //skip internal:* - //check transport audit enabled - //check category enabled - //check action - //check ignoreAuditUsers + // skip internal:* + // check transport audit enabled + // check category enabled + // check action + // check ignoreAuditUsers } - private boolean checkComplianceFilter(final AuditCategory category, final String effectiveUser, Origin origin, ComplianceConfig complianceConfig) { + private boolean checkComplianceFilter( + final AuditCategory category, + final String effectiveUser, + Origin origin, + ComplianceConfig complianceConfig + ) { final boolean isTraceEnabled = log.isTraceEnabled(); if (isTraceEnabled) { log.trace("Check for COMPLIANCE category:{}, effectiveUser:{}, origin: {}", category, effectiveUser, origin); } - if(origin == Origin.LOCAL && effectiveUser == null && category != AuditCategory.COMPLIANCE_EXTERNAL_CONFIG) { + if (origin == Origin.LOCAL && effectiveUser == null && category != AuditCategory.COMPLIANCE_EXTERNAL_CONFIG) { if (isTraceEnabled) { log.trace("Skipped compliance log message because of null user and local origin"); } return false; } - if(category == AuditCategory.COMPLIANCE_DOC_READ || category == AuditCategory.COMPLIANCE_INTERNAL_CONFIG_READ) { + if (category == AuditCategory.COMPLIANCE_DOC_READ || category == AuditCategory.COMPLIANCE_INTERNAL_CONFIG_READ) { if (effectiveUser != null && complianceConfig.isComplianceReadAuditDisabled(effectiveUser)) { @@ -678,7 +881,7 @@ private boolean checkComplianceFilter(final AuditCategory category, final String } } - if(category == AuditCategory.COMPLIANCE_DOC_WRITE || category == AuditCategory.COMPLIANCE_INTERNAL_CONFIG_WRITE) { + if (category == AuditCategory.COMPLIANCE_DOC_WRITE || category == AuditCategory.COMPLIANCE_INTERNAL_CONFIG_WRITE) { if (effectiveUser != null && complianceConfig.isComplianceWriteAuditDisabled(effectiveUser)) { if (isTraceEnabled) { @@ -695,7 +898,12 @@ private boolean checkComplianceFilter(final AuditCategory category, final String boolean checkRestFilter(final AuditCategory category, final String effectiveUser, RestRequest request) { final boolean isTraceEnabled = log.isTraceEnabled(); if (isTraceEnabled) { - log.trace("Check for REST category:{}, effectiveUser:{}, request:{}", category, effectiveUser, request==null?null:request.path()); + log.trace( + "Check for REST category:{}, effectiveUser:{}, request:{}", + category, + effectiveUser, + request == null ? null : request.path() + ); } if (!auditConfigFilter.isRestApiAuditEnabled()) { @@ -729,13 +937,11 @@ boolean checkRestFilter(final AuditCategory category, final String effectiveUser return false; } - - //check rest audit enabled - //check category enabled - //check action - //check ignoreAuditUsers + // check rest audit enabled + // check category enabled + // check action + // check ignoreAuditUsers } - protected abstract void save(final AuditMessage msg); } diff --git a/src/main/java/org/opensearch/security/auditlog/impl/AuditCategory.java b/src/main/java/org/opensearch/security/auditlog/impl/AuditCategory.java index 20f8b4e777..caf6938b14 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AuditCategory.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AuditCategory.java @@ -33,13 +33,8 @@ public enum AuditCategory { COMPLIANCE_INTERNAL_CONFIG_WRITE; public static Set parse(final Collection categories) { - if (categories.isEmpty()) - return Collections.emptySet(); + if (categories.isEmpty()) return Collections.emptySet(); - return categories - .stream() - .map(String::toUpperCase) - .map(AuditCategory::valueOf) - .collect(ImmutableSet.toImmutableSet()); + return categories.stream().map(String::toUpperCase).map(AuditCategory::valueOf).collect(ImmutableSet.toImmutableSet()); } } diff --git a/src/main/java/org/opensearch/security/auditlog/impl/AuditLogImpl.java b/src/main/java/org/opensearch/security/auditlog/impl/AuditLogImpl.java index be5068fc14..a09c6a694e 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AuditLogImpl.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AuditLogImpl.java @@ -40,54 +40,58 @@ public final class AuditLogImpl extends AbstractAuditLog { - private final AuditMessageRouter messageRouter; - private final Settings settings; - private final boolean messageRouterEnabled; - private volatile boolean enabled; - private final Thread shutdownHook; - - public AuditLogImpl(final Settings settings, - final Path configPath, - final Client clientProvider, - final ThreadPool threadPool, - final IndexNameExpressionResolver resolver, - final ClusterService clusterService) { - this(settings, configPath, clientProvider, threadPool, resolver, clusterService, null); - } + private final AuditMessageRouter messageRouter; + private final Settings settings; + private final boolean messageRouterEnabled; + private volatile boolean enabled; + private final Thread shutdownHook; + + public AuditLogImpl( + final Settings settings, + final Path configPath, + final Client clientProvider, + final ThreadPool threadPool, + final IndexNameExpressionResolver resolver, + final ClusterService clusterService + ) { + this(settings, configPath, clientProvider, threadPool, resolver, clusterService, null); + } @SuppressWarnings("removal") - public AuditLogImpl(final Settings settings, - final Path configPath, - final Client clientProvider, - final ThreadPool threadPool, - final IndexNameExpressionResolver resolver, - final ClusterService clusterService, - final Environment environment) { - super(settings, threadPool, resolver, clusterService, environment); - this.settings = settings; - this.messageRouter = new AuditMessageRouter(settings, clientProvider, threadPool, configPath); - this.messageRouterEnabled = this.messageRouter.isEnabled(); - - log.info("Message routing enabled: {}", this.messageRouterEnabled); - - SpecialPermission.check(); - shutdownHook = AccessController.doPrivileged((PrivilegedAction) this::addShutdownHook); - log.debug("Shutdown hook {} registered", shutdownHook); - } - - @Subscribe - public void setConfig(final AuditConfig auditConfig) { - enabled = auditConfig.isEnabled() && messageRouterEnabled; - onAuditConfigFilterChanged(auditConfig.getFilter()); - onComplianceConfigChanged(auditConfig.getCompliance()); - } - - @Override - protected void enableRoutes() { - if (messageRouterEnabled) { - messageRouter.enableRoutes(settings); - } - } + public AuditLogImpl( + final Settings settings, + final Path configPath, + final Client clientProvider, + final ThreadPool threadPool, + final IndexNameExpressionResolver resolver, + final ClusterService clusterService, + final Environment environment + ) { + super(settings, threadPool, resolver, clusterService, environment); + this.settings = settings; + this.messageRouter = new AuditMessageRouter(settings, clientProvider, threadPool, configPath); + this.messageRouterEnabled = this.messageRouter.isEnabled(); + + log.info("Message routing enabled: {}", this.messageRouterEnabled); + + SpecialPermission.check(); + shutdownHook = AccessController.doPrivileged((PrivilegedAction) this::addShutdownHook); + log.debug("Shutdown hook {} registered", shutdownHook); + } + + @Subscribe + public void setConfig(final AuditConfig auditConfig) { + enabled = auditConfig.isEnabled() && messageRouterEnabled; + onAuditConfigFilterChanged(auditConfig.getFilter()); + onComplianceConfigChanged(auditConfig.getCompliance()); + } + + @Override + protected void enableRoutes() { + if (messageRouterEnabled) { + messageRouter.enableRoutes(settings); + } + } private Thread addShutdownHook() { Thread shutdownHook = new Thread(() -> messageRouter.close()); @@ -119,123 +123,123 @@ public void close() throws IOException { } } - @Override - protected void save(final AuditMessage msg) { - if (enabled) { - messageRouter.route(msg); - } - } - - @Override - public void logFailedLogin(String effectiveUser, boolean securityAdmin, String initiatingUser, RestRequest request) { - if (enabled) { - super.logFailedLogin(effectiveUser, securityAdmin, initiatingUser, request); - } - } - - @Override - public void logSucceededLogin(String effectiveUser, boolean securityAdmin, String initiatingUser, RestRequest request) { - if (enabled) { - super.logSucceededLogin(effectiveUser, securityAdmin, initiatingUser, request); - } - } - - @Override - public void logMissingPrivileges(String privilege, String effectiveUser, RestRequest request) { - if (enabled) { - super.logMissingPrivileges(privilege, effectiveUser, request); - } - } - - @Override - public void logGrantedPrivileges(String effectiveUser, RestRequest request) { - if (enabled) { - super.logGrantedPrivileges(effectiveUser, request); - } - } - - @Override - public void logMissingPrivileges(String privilege, TransportRequest request, Task task) { - if (enabled) { - super.logMissingPrivileges(privilege, request, task); - } - } - - @Override - public void logGrantedPrivileges(String privilege, TransportRequest request, Task task) { - if (enabled) { - super.logGrantedPrivileges(privilege, request, task); - } - } - - @Override - public void logIndexEvent(String privilege, TransportRequest request, Task task) { - if (enabled) { - super.logIndexEvent(privilege, request, task); - } - } - - @Override - public void logBadHeaders(TransportRequest request, String action, Task task) { - if (enabled) { - super.logBadHeaders(request, action, task); - } - } - - @Override - public void logBadHeaders(RestRequest request) { - if (enabled) { - super.logBadHeaders(request); - } - } - - @Override - public void logSecurityIndexAttempt (TransportRequest request, String action, Task task) { - if (enabled) { - super.logSecurityIndexAttempt(request, action, task); - } - } - - @Override - public void logSSLException(TransportRequest request, Throwable t, String action, Task task) { - if (enabled) { - super.logSSLException(request, t, action, task); - } - } - - @Override - public void logSSLException(RestRequest request, Throwable t) { - if (enabled) { - super.logSSLException(request, t); - } - } - - @Override - public void logDocumentRead(String index, String id, ShardId shardId, Map fieldNameValues) { - if (enabled) { - super.logDocumentRead(index, id, shardId, fieldNameValues); - } - } - - @Override - public void logDocumentWritten(ShardId shardId, GetResult originalResult, Index currentIndex, IndexResult result) { - if (enabled) { - super.logDocumentWritten(shardId, originalResult, currentIndex, result); - } - } - - @Override - public void logDocumentDeleted(ShardId shardId, Delete delete, DeleteResult result) { - if (enabled) { - super.logDocumentDeleted(shardId, delete, result); - } - } - - @Override - protected void logExternalConfig() { - if (enabled) { - super.logExternalConfig(); - } - } + @Override + protected void save(final AuditMessage msg) { + if (enabled) { + messageRouter.route(msg); + } + } + + @Override + public void logFailedLogin(String effectiveUser, boolean securityAdmin, String initiatingUser, RestRequest request) { + if (enabled) { + super.logFailedLogin(effectiveUser, securityAdmin, initiatingUser, request); + } + } + + @Override + public void logSucceededLogin(String effectiveUser, boolean securityAdmin, String initiatingUser, RestRequest request) { + if (enabled) { + super.logSucceededLogin(effectiveUser, securityAdmin, initiatingUser, request); + } + } + + @Override + public void logMissingPrivileges(String privilege, String effectiveUser, RestRequest request) { + if (enabled) { + super.logMissingPrivileges(privilege, effectiveUser, request); + } + } + + @Override + public void logGrantedPrivileges(String effectiveUser, RestRequest request) { + if (enabled) { + super.logGrantedPrivileges(effectiveUser, request); + } + } + + @Override + public void logMissingPrivileges(String privilege, TransportRequest request, Task task) { + if (enabled) { + super.logMissingPrivileges(privilege, request, task); + } + } + + @Override + public void logGrantedPrivileges(String privilege, TransportRequest request, Task task) { + if (enabled) { + super.logGrantedPrivileges(privilege, request, task); + } + } + + @Override + public void logIndexEvent(String privilege, TransportRequest request, Task task) { + if (enabled) { + super.logIndexEvent(privilege, request, task); + } + } + + @Override + public void logBadHeaders(TransportRequest request, String action, Task task) { + if (enabled) { + super.logBadHeaders(request, action, task); + } + } + + @Override + public void logBadHeaders(RestRequest request) { + if (enabled) { + super.logBadHeaders(request); + } + } + + @Override + public void logSecurityIndexAttempt(TransportRequest request, String action, Task task) { + if (enabled) { + super.logSecurityIndexAttempt(request, action, task); + } + } + + @Override + public void logSSLException(TransportRequest request, Throwable t, String action, Task task) { + if (enabled) { + super.logSSLException(request, t, action, task); + } + } + + @Override + public void logSSLException(RestRequest request, Throwable t) { + if (enabled) { + super.logSSLException(request, t); + } + } + + @Override + public void logDocumentRead(String index, String id, ShardId shardId, Map fieldNameValues) { + if (enabled) { + super.logDocumentRead(index, id, shardId, fieldNameValues); + } + } + + @Override + public void logDocumentWritten(ShardId shardId, GetResult originalResult, Index currentIndex, IndexResult result) { + if (enabled) { + super.logDocumentWritten(shardId, originalResult, currentIndex, result); + } + } + + @Override + public void logDocumentDeleted(ShardId shardId, Delete delete, DeleteResult result) { + if (enabled) { + super.logDocumentDeleted(shardId, delete, result); + } + } + + @Override + protected void logExternalConfig() { + if (enabled) { + super.logExternalConfig(); + } + } } diff --git a/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java b/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java index 8931d44690..9b1d70d41c 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/AuditMessage.java @@ -55,13 +55,14 @@ public final class AuditMessage { - //clustername and cluster uuid + // clustername and cluster uuid private static final WildcardMatcher AUTHORIZATION_HEADER = WildcardMatcher.from("Authorization", false); private static final String SENSITIVE_KEY = "password"; private static final String SENSITIVE_REPLACEMENT_VALUE = "__SENSITIVE__"; - private static final Pattern SENSITIVE_PATHS = - Pattern.compile( "/(" + LEGACY_OPENDISTRO_PREFIX + "|" + PLUGINS_PREFIX + ")/api/(account.*|internalusers.*|user.*)"); + private static final Pattern SENSITIVE_PATHS = Pattern.compile( + "/(" + LEGACY_OPENDISTRO_PREFIX + "|" + PLUGINS_PREFIX + ")/api/(account.*|internalusers.*|user.*)" + ); @VisibleForTesting public static final Pattern BCRYPT_HASH = Pattern.compile("\\$2[ayb]\\$.{56}"); @@ -93,8 +94,8 @@ public final class AuditMessage { public static final String TRANSPORT_REQUEST_HEADERS = "audit_transport_headers"; public static final String ID = "audit_trace_doc_id"; - //public static final String TYPES = "audit_trace_doc_types"; - //public static final String SOURCE = "audit_trace_doc_source"; + // public static final String TYPES = "audit_trace_doc_types"; + // public static final String SOURCE = "audit_trace_doc_source"; public static final String INDICES = "audit_trace_indices"; public static final String SHARD_ID = "audit_trace_shard_id"; public static final String RESOLVED_INDICES = "audit_trace_resolved_indices"; @@ -111,8 +112,8 @@ public final class AuditMessage { public static final String COMPLIANCE_DIFF_CONTENT = "audit_compliance_diff_content"; public static final String COMPLIANCE_FILE_INFOS = "audit_compliance_file_infos"; - //public static final String COMPLIANCE_DIFF_STORED_IS_NOOP = "audit_compliance_diff_stored_is_noop"; - //public static final String COMPLIANCE_STORED_FIELDS_CONTENT = "audit_compliance_stored_fields_content"; + // public static final String COMPLIANCE_DIFF_STORED_IS_NOOP = "audit_compliance_diff_stored_is_noop"; + // public static final String COMPLIANCE_STORED_FIELDS_CONTENT = "audit_compliance_stored_fields_content"; public static final String REQUEST_LAYER = "audit_request_layer"; @@ -135,11 +136,11 @@ public AuditMessage(final AuditCategory msgCategory, final ClusterService cluste auditInfo.put(NODE_NAME, Objects.requireNonNull(clusterService).localNode().getName()); auditInfo.put(CLUSTER_NAME, Objects.requireNonNull(clusterService).getClusterName().value()); - if(origin != null) { + if (origin != null) { auditInfo.put(ORIGIN, origin); } - if(layer != null) { + if (layer != null) { auditInfo.put(REQUEST_LAYER, layer); } } @@ -197,25 +198,25 @@ void addSecurityConfigWriteDiffSource(final String diff, final String id) { addComplianceWriteDiffSource(redactSecurityConfigContent(diff, id)); } -// public void addComplianceWriteStoredFields0(String diff) { -// if (diff != null && !diff.isEmpty()) { -// auditInfo.put(COMPLIANCE_STORED_FIELDS_CONTENT, diff); -// //auditInfo.put(COMPLIANCE_DIFF_STORED_IS_NOOP, false); -// } -// } + // public void addComplianceWriteStoredFields0(String diff) { + // if (diff != null && !diff.isEmpty()) { + // auditInfo.put(COMPLIANCE_STORED_FIELDS_CONTENT, diff); + // //auditInfo.put(COMPLIANCE_DIFF_STORED_IS_NOOP, false); + // } + // } public void addTupleToRequestBody(Tuple xContentTuple) { if (xContentTuple != null) { try { auditInfo.put(REQUEST_BODY, XContentHelper.convertToJson(xContentTuple.v2(), false, xContentTuple.v1())); } catch (Exception e) { - auditInfo.put(REQUEST_BODY, "ERROR: Unable to convert to json because of "+e.toString()); + auditInfo.put(REQUEST_BODY, "ERROR: Unable to convert to json because of " + e.toString()); } } } public void addMapToRequestBody(Map map) { - if(map != null) { + if (map != null) { auditInfo.put(REQUEST_BODY, Utils.convertStructuredMapToJson(map)); } } @@ -289,10 +290,10 @@ public void addType(String type) { public void addFileInfos(Map paths) { if (paths != null && !paths.isEmpty()) { List infos = new ArrayList<>(); - for(Entry path: paths.entrySet()) { + for (Entry path : paths.entrySet()) { try { - if(Files.isReadable(path.getValue())) { + if (Files.isReadable(path.getValue())) { final String chcksm = DigestUtils.sha256Hex(Files.readAllBytes(path.getValue())); FileTime lm = Files.getLastModifiedTime(path.getValue(), LinkOption.NOFOLLOW_LINKS); Map innerInfos = new HashMap<>(); @@ -303,7 +304,7 @@ public void addFileInfos(Map paths) { infos.add(innerInfos); } } catch (Throwable e) { - //ignore non readable files + // ignore non readable files } } auditInfo.put(COMPLIANCE_FILE_INFOS, infos); @@ -330,29 +331,29 @@ public void addResolvedIndices(String[] resolvedIndices) { } public void addTaskId(long id) { - auditInfo.put(TASK_ID, auditInfo.get(NODE_ID)+":"+id); + auditInfo.put(TASK_ID, auditInfo.get(NODE_ID) + ":" + id); } public void addShardId(ShardId id) { - if(id != null) { + if (id != null) { auditInfo.put(SHARD_ID, id.getId()); } - } + } public void addTaskParentId(String id) { - if(id != null) { + if (id != null) { auditInfo.put(TASK_PARENT_ID, id); } } - public void addRestParams(Map params) { - if(params != null && !params.isEmpty()) { + public void addRestParams(Map params) { + if (params != null && !params.isEmpty()) { auditInfo.put(REST_REQUEST_PARAMS, new HashMap<>(params)); } } - public void addRestHeaders(Map> headers, boolean excludeSensitiveHeaders) { - if(headers != null && !headers.isEmpty()) { + public void addRestHeaders(Map> headers, boolean excludeSensitiveHeaders) { + if (headers != null && !headers.isEmpty()) { final Map> headersClone = new HashMap<>(headers); if (excludeSensitiveHeaders) { headersClone.keySet().removeIf(AUTHORIZATION_HEADER); @@ -377,10 +378,11 @@ void addRestRequestInfo(final RestRequest request, final AuditConfig.Filter filt if (filter.shouldLogRequestBody() && request.hasContentOrSourceParam()) { try { final Tuple xContentTuple = request.contentOrSourceParam(); - final String requestBody = XContentHelper.convertToJson(xContentTuple.v2(), false, xContentTuple.v1()); - if (path != null && requestBody != null - && SENSITIVE_PATHS.matcher(path).matches() - && requestBody.contains(SENSITIVE_KEY)) { + final String requestBody = XContentHelper.convertToJson(xContentTuple.v2(), false, xContentTuple.v1()); + if (path != null + && requestBody != null + && SENSITIVE_PATHS.matcher(path).matches() + && requestBody.contains(SENSITIVE_KEY)) { auditInfo.put(REQUEST_BODY, SENSITIVE_REPLACEMENT_VALUE); } else { auditInfo.put(REQUEST_BODY, requestBody); @@ -392,8 +394,8 @@ void addRestRequestInfo(final RestRequest request, final AuditConfig.Filter filt } } - public void addTransportHeaders(Map headers, boolean excludeSensitiveHeaders) { - if(headers != null && !headers.isEmpty()) { + public void addTransportHeaders(Map headers, boolean excludeSensitiveHeaders) { + if (headers != null && !headers.isEmpty()) { final Map headersClone = new HashMap<>(headers); if (excludeSensitiveHeaders) { headersClone.keySet().removeIf(AUTHORIZATION_HEADER); @@ -403,7 +405,7 @@ public void addTransportHeaders(Map headers, boolean excludeSensi } public void addComplianceOperation(Operation op) { - if(op != null) { + if (op != null) { auditInfo.put(COMPLIANCE_OPERATION, op); } } @@ -413,7 +415,7 @@ public void addComplianceDocVersion(long version) { } public Map getAsMap() { - return new HashMap<>(this.auditInfo); + return new HashMap<>(this.auditInfo); } public String getInitiatingUser() { @@ -432,9 +434,9 @@ public RestRequest.Method getRequestMethod() { return (RestRequest.Method) this.auditInfo.get(REST_REQUEST_METHOD); } - public AuditCategory getCategory() { - return msgCategory; - } + public AuditCategory getCategory() { + return msgCategory; + } public Origin getOrigin() { return (Origin) this.auditInfo.get(ORIGIN); @@ -460,14 +462,14 @@ public String getDocId() { return (String) this.auditInfo.get(ID); } - @Override - public String toString() { - try { - return org.opensearch.common.Strings.toString(JsonXContent.contentBuilder().map(getAsMap())); - } catch (final IOException e) { - throw ExceptionsHelper.convertToOpenSearchException(e); - } - } + @Override + public String toString() { + try { + return org.opensearch.common.Strings.toString(JsonXContent.contentBuilder().map(getAsMap())); + } catch (final IOException e) { + throw ExceptionsHelper.convertToOpenSearchException(e); + } + } public String toPrettyString() { try { @@ -477,34 +479,34 @@ public String toPrettyString() { } } - public String toText() { - StringBuilder builder = new StringBuilder(); - for (Entry entry : getAsMap().entrySet()) { - addIfNonEmpty(builder, entry.getKey(), stringOrNull(entry.getValue())); - } - return builder.toString(); - } - - public final String toJson() { - return this.toString(); - } - - public String toUrlParameters() { - URIBuilder builder = new URIBuilder(); - for (Entry entry : getAsMap().entrySet()) { - builder.addParameter(entry.getKey(), stringOrNull(entry.getValue())); - } - return builder.toString(); - } - - protected static void addIfNonEmpty(StringBuilder builder, String key, String value) { - if (!Strings.isEmpty(value)) { - if (builder.length() > 0) { - builder.append("\n"); - } - builder.append(key).append(": ").append(value); - } - } + public String toText() { + StringBuilder builder = new StringBuilder(); + for (Entry entry : getAsMap().entrySet()) { + addIfNonEmpty(builder, entry.getKey(), stringOrNull(entry.getValue())); + } + return builder.toString(); + } + + public final String toJson() { + return this.toString(); + } + + public String toUrlParameters() { + URIBuilder builder = new URIBuilder(); + for (Entry entry : getAsMap().entrySet()) { + builder.addParameter(entry.getKey(), stringOrNull(entry.getValue())); + } + return builder.toString(); + } + + protected static void addIfNonEmpty(StringBuilder builder, String key, String value) { + if (!Strings.isEmpty(value)) { + if (builder.length() > 0) { + builder.append("\n"); + } + builder.append(key).append(": ").append(value); + } + } private String currentTime() { DateTime dt = new DateTime(DateTimeZone.UTC); @@ -517,7 +519,7 @@ private String formatTime(long epoch) { } protected String stringOrNull(Object object) { - if(object == null) { + if (object == null) { return null; } diff --git a/src/main/java/org/opensearch/security/auditlog/impl/RequestResolver.java b/src/main/java/org/opensearch/security/auditlog/impl/RequestResolver.java index 5b4881d1eb..ecf7a2bd36 100644 --- a/src/main/java/org/opensearch/security/auditlog/impl/RequestResolver.java +++ b/src/main/java/org/opensearch/security/auditlog/impl/RequestResolver.java @@ -64,54 +64,56 @@ public final class RequestResolver { private static final Logger log = LogManager.getLogger(RequestResolver.class); public static List resolve( - final AuditCategory category, - final Origin origin, - final String action, - final String privilege, - final String effectiveUser, - final Boolean securityadmin, - final String initiatingUser, - final TransportAddress remoteAddress, - final TransportRequest request, - final Map headers, - final Task task, - final IndexNameExpressionResolver resolver, - final ClusterService cs, - final Settings settings, - final boolean logRequestBody, - final boolean resolveIndices, - final boolean resolveBulk, - final String securityIndex, - final boolean excludeSensitiveHeaders, - final Throwable exception) { - - if(resolveBulk && request instanceof BulkShardRequest) { + final AuditCategory category, + final Origin origin, + final String action, + final String privilege, + final String effectiveUser, + final Boolean securityadmin, + final String initiatingUser, + final TransportAddress remoteAddress, + final TransportRequest request, + final Map headers, + final Task task, + final IndexNameExpressionResolver resolver, + final ClusterService cs, + final Settings settings, + final boolean logRequestBody, + final boolean resolveIndices, + final boolean resolveBulk, + final String securityIndex, + final boolean excludeSensitiveHeaders, + final Throwable exception + ) { + + if (resolveBulk && request instanceof BulkShardRequest) { final BulkItemRequest[] innerRequests = ((BulkShardRequest) request).items(); final List messages = new ArrayList(innerRequests.length); - for(BulkItemRequest ar: innerRequests) { + for (BulkItemRequest ar : innerRequests) { final DocWriteRequest innerRequest = ar.request(); final AuditMessage msg = resolveInner( - category, - effectiveUser, - securityadmin, - initiatingUser, - remoteAddress, - action, - privilege, - origin, - innerRequest, - headers, - task, - resolver, - cs, - settings, - logRequestBody, - resolveIndices, - securityIndex, - excludeSensitiveHeaders, - exception); - msg.addShardId(((BulkShardRequest) request).shardId()); + category, + effectiveUser, + securityadmin, + initiatingUser, + remoteAddress, + action, + privilege, + origin, + innerRequest, + headers, + task, + resolver, + cs, + settings, + logRequestBody, + resolveIndices, + securityIndex, + excludeSensitiveHeaders, + exception + ); + msg.addShardId(((BulkShardRequest) request).shardId()); messages.add(msg); } @@ -119,17 +121,18 @@ public static List resolve( return messages; } - if(request instanceof BulkShardRequest) { + if (request instanceof BulkShardRequest) { - if(category != AuditCategory.FAILED_LOGIN - && category != AuditCategory.MISSING_PRIVILEGES - && category != AuditCategory.OPENDISTRO_SECURITY_INDEX_ATTEMPT) { + if (category != AuditCategory.FAILED_LOGIN + && category != AuditCategory.MISSING_PRIVILEGES + && category != AuditCategory.OPENDISTRO_SECURITY_INDEX_ATTEMPT) { return Collections.emptyList(); } } - return Collections.singletonList(resolveInner( + return Collections.singletonList( + resolveInner( category, effectiveUser, securityadmin, @@ -148,29 +151,32 @@ public static List resolve( resolveIndices, securityIndex, excludeSensitiveHeaders, - exception)); + exception + ) + ); } - - private static AuditMessage resolveInner(final AuditCategory category, - final String effectiveUser, - final Boolean securityadmin, - final String initiatingUser, - final TransportAddress remoteAddress, - final String action, - final String priv, - final Origin origin, - final Object request, - final Map headers, - final Task task, - final IndexNameExpressionResolver resolver, - final ClusterService cs, - final Settings settings, - final boolean logRequestBody, - final boolean resolveIndices, - final String securityIndex, - final boolean excludeSensitiveHeaders, - final Throwable exception) { + private static AuditMessage resolveInner( + final AuditCategory category, + final String effectiveUser, + final Boolean securityadmin, + final String initiatingUser, + final TransportAddress remoteAddress, + final String action, + final String priv, + final Origin origin, + final Object request, + final Map headers, + final Task task, + final IndexNameExpressionResolver resolver, + final ClusterService cs, + final Settings settings, + final boolean logRequestBody, + final boolean resolveIndices, + final String securityIndex, + final boolean excludeSensitiveHeaders, + final Throwable exception + ) { final AuditMessage msg = new AuditMessage(category, cs, origin, Origin.TRANSPORT); msg.addInitiatingUser(initiatingUser); @@ -178,11 +184,11 @@ private static AuditMessage resolveInner(final AuditCategory category, msg.addRemoteAddress(remoteAddress); msg.addAction(action); - if(request != null) { + if (request != null) { msg.addRequestType(request.getClass().getSimpleName()); } - if(securityadmin != null) { + if (securityadmin != null) { msg.addIsAdminDn(securityadmin); } @@ -190,14 +196,14 @@ private static AuditMessage resolveInner(final AuditCategory category, msg.addPrivilege(priv); msg.addTransportHeaders(headers, excludeSensitiveHeaders); - if(task != null) { + if (task != null) { msg.addTaskId(task.getId()); - if(task.getParentTaskId() != null && task.getParentTaskId().isSet()) { + if (task.getParentTaskId() != null && task.getParentTaskId().isSet()) { msg.addTaskParentId(task.getParentTaskId().toString()); } } - //attempt to resolve indices/types/id/source + // attempt to resolve indices/types/id/source if (request instanceof MultiGetRequest.Item) { final MultiGetRequest.Item item = (MultiGetRequest.Item) request; final String[] indices = arrayOrEmpty(item.indices()); @@ -221,7 +227,7 @@ private static AuditMessage resolveInner(final AuditCategory category, } else if (request instanceof DeleteIndexRequest) { final DeleteIndexRequest dir = (DeleteIndexRequest) request; final String[] indices = arrayOrEmpty(dir.indices()); - //dir id alle id's beim schreiben protokolloieren + // dir id alle id's beim schreiben protokolloieren addIndicesSourceSafe(msg, indices, resolver, cs, null, null, settings, resolveIndices, logRequestBody, false, securityIndex); } else if (request instanceof IndexRequest) { final IndexRequest ir = (IndexRequest) request; @@ -229,7 +235,19 @@ private static AuditMessage resolveInner(final AuditCategory category, final String id = ir.id(); msg.addShardId(ir.shardId()); msg.addId(id); - addIndicesSourceSafe(msg, indices, resolver, cs, ir.getContentType(), ir.source(), settings, resolveIndices, logRequestBody, true, securityIndex); + addIndicesSourceSafe( + msg, + indices, + resolver, + cs, + ir.getContentType(), + ir.source(), + settings, + resolveIndices, + logRequestBody, + true, + securityIndex + ); } else if (request instanceof DeleteRequest) { final DeleteRequest dr = (DeleteRequest) request; final String[] indices = arrayOrEmpty(dr.indices()); @@ -243,10 +261,10 @@ private static AuditMessage resolveInner(final AuditCategory category, final String id = ur.id(); msg.addId(id); addIndicesSourceSafe(msg, indices, resolver, cs, null, null, settings, resolveIndices, logRequestBody, false, securityIndex); - if(logRequestBody) { + if (logRequestBody) { if (ur.doc() != null) { - msg.addTupleToRequestBody(ur.doc() == null ? null :convertSource(ur.doc().getContentType(), ur.doc().source())); + msg.addTupleToRequestBody(ur.doc() == null ? null : convertSource(ur.doc().getContentType(), ur.doc().source())); } if (ur.script() != null) { @@ -263,10 +281,22 @@ private static AuditMessage resolveInner(final AuditCategory category, final SearchRequest sr = (SearchRequest) request; final String[] indices = arrayOrEmpty(sr.indices()); - Map sourceAsMap = sr.source() == null? null:Utils.convertJsonToxToStructuredMap(sr.source()); - addIndicesSourceSafe(msg, indices, resolver, cs, XContentType.JSON, sourceAsMap, settings, resolveIndices, logRequestBody, false, securityIndex); + Map sourceAsMap = sr.source() == null ? null : Utils.convertJsonToxToStructuredMap(sr.source()); + addIndicesSourceSafe( + msg, + indices, + resolver, + cs, + XContentType.JSON, + sourceAsMap, + settings, + resolveIndices, + logRequestBody, + false, + securityIndex + ); } else if (request instanceof ClusterUpdateSettingsRequest) { - if(logRequestBody) { + if (logRequestBody) { final ClusterUpdateSettingsRequest cusr = (ClusterUpdateSettingsRequest) request; final Settings persistentSettings = cusr.persistentSettings(); final Settings transientSettings = cusr.transientSettings(); @@ -276,31 +306,42 @@ private static AuditMessage resolveInner(final AuditCategory category, builder = XContentFactory.jsonBuilder(); builder.startObject(); - if(persistentSettings != null) { + if (persistentSettings != null) { builder.field("persistent_settings", Utils.convertJsonToxToStructuredMap(persistentSettings)); } - if(transientSettings != null) { + if (transientSettings != null) { builder.field("transient_settings", Utils.convertJsonToxToStructuredMap(persistentSettings)); } builder.endObject(); - msg.addUnescapedJsonToRequestBody(builder == null?null:Strings.toString(builder)); + msg.addUnescapedJsonToRequestBody(builder == null ? null : Strings.toString(builder)); } catch (IOException e) { log.error(e.toString()); } finally { - if(builder != null) { + if (builder != null) { builder.close(); } } - - } + } } else if (request instanceof ReindexRequest) { final IndexRequest ir = ((ReindexRequest) request).getDestination(); final String[] indices = arrayOrEmpty(ir.indices()); final String id = ir.id(); msg.addShardId(ir.shardId()); msg.addId(id); - addIndicesSourceSafe(msg, indices, resolver, cs, ir.getContentType(), ir.source(), settings, resolveIndices, logRequestBody, true, securityIndex); + addIndicesSourceSafe( + msg, + indices, + resolver, + cs, + ir.getContentType(), + ir.source(), + settings, + resolveIndices, + logRequestBody, + true, + securityIndex + ); } else if (request instanceof DeleteByQueryRequest) { final DeleteByQueryRequest ir = (DeleteByQueryRequest) request; final String[] indices = arrayOrEmpty(ir.indices()); @@ -315,18 +356,18 @@ private static AuditMessage resolveInner(final AuditCategory category, String[] indices = new String[0]; msg.addIndices(indices); - if(ci != null) { - indices = new String[]{ci.getName()}; + if (ci != null) { + indices = new String[] { ci.getName() }; } - if(logRequestBody) { + if (logRequestBody) { msg.addUnescapedJsonToRequestBody(pr.source()); } - if(resolveIndices) { + if (resolveIndices) { msg.addResolvedIndices(indices); } - } else if (request instanceof IndicesRequest) { //less specific + } else if (request instanceof IndicesRequest) { // less specific final IndicesRequest ir = (IndicesRequest) request; final String[] indices = arrayOrEmpty(ir.indices()); addIndicesSourceSafe(msg, indices, resolver, cs, null, null, settings, resolveIndices, logRequestBody, false, securityIndex); @@ -335,66 +376,70 @@ private static AuditMessage resolveInner(final AuditCategory category, return msg; } - private static void addIndicesSourceSafe(final AuditMessage msg, - final String[] indices, - final IndexNameExpressionResolver resolver, - final ClusterService cs, - final XContentType xContentType, - final Object source, - final Settings settings, - boolean resolveIndices, - final boolean addSource, - final boolean sourceIsSensitive, - final String securityIndex) { - - if(addSource) { + private static void addIndicesSourceSafe( + final AuditMessage msg, + final String[] indices, + final IndexNameExpressionResolver resolver, + final ClusterService cs, + final XContentType xContentType, + final Object source, + final Settings settings, + boolean resolveIndices, + final boolean addSource, + final boolean sourceIsSensitive, + final String securityIndex + ) { + + if (addSource) { resolveIndices = true; } - final String[] _indices = indices == null?new String[0]:indices; + final String[] _indices = indices == null ? new String[0] : indices; msg.addIndices(_indices); final Set allIndices; - if(resolveIndices) { - final String[] resolvedIndices = (resolver==null)?new String[0]:resolver.concreteIndexNames(cs.state(), IndicesOptions.lenientExpandOpen(), indices); + if (resolveIndices) { + final String[] resolvedIndices = (resolver == null) + ? new String[0] + : resolver.concreteIndexNames(cs.state(), IndicesOptions.lenientExpandOpen(), indices); msg.addResolvedIndices(resolvedIndices); - allIndices = new HashSet(resolvedIndices.length+_indices.length); + allIndices = new HashSet(resolvedIndices.length + _indices.length); allIndices.addAll(Arrays.asList(_indices)); allIndices.addAll(Arrays.asList(resolvedIndices)); - if(allIndices.contains("_all")) { - allIndices.add("*"); //TODO: maybe replace allIndices instead of add? + if (allIndices.contains("_all")) { + allIndices.add("*"); // TODO: maybe replace allIndices instead of add? } } else { allIndices = new HashSet(_indices.length); allIndices.addAll(Arrays.asList(_indices)); - if(allIndices.contains("_all")) { - allIndices.add("*"); //TODO: maybe replace allIndices instead of add? + if (allIndices.contains("_all")) { + allIndices.add("*"); // TODO: maybe replace allIndices instead of add? } } final WildcardMatcher allIndicesMatcher = WildcardMatcher.from(allIndices); - if(addSource) { - if(sourceIsSensitive && source != null) { - if(!allIndicesMatcher.test(securityIndex)) { - if(source instanceof BytesReference) { - msg.addTupleToRequestBody(convertSource(xContentType, (BytesReference) source)); + if (addSource) { + if (sourceIsSensitive && source != null) { + if (!allIndicesMatcher.test(securityIndex)) { + if (source instanceof BytesReference) { + msg.addTupleToRequestBody(convertSource(xContentType, (BytesReference) source)); } else { msg.addMapToRequestBody((Map) source); } } - } else if(source != null) { - if(source instanceof BytesReference) { + } else if (source != null) { + if (source instanceof BytesReference) { msg.addTupleToRequestBody(convertSource(xContentType, (BytesReference) source)); - } else { - msg.addMapToRequestBody((Map) source); - } + } else { + msg.addMapToRequestBody((Map) source); + } } } } private static Tuple convertSource(XContentType type, BytesReference bytes) { - if(type == null) { + if (type == null) { type = XContentType.JSON; } @@ -402,11 +447,11 @@ private static Tuple convertSource(XContentType ty } private static String[] arrayOrEmpty(String[] array) { - if(array == null) { + if (array == null) { return new String[0]; } - if(array.length == 1 && array[0] == null) { + if (array.length == 1 && array[0] == null) { return new String[0]; } diff --git a/src/main/java/org/opensearch/security/auditlog/routing/AsyncStoragePool.java b/src/main/java/org/opensearch/security/auditlog/routing/AsyncStoragePool.java index bbea998b8d..494d67aba6 100644 --- a/src/main/java/org/opensearch/security/auditlog/routing/AsyncStoragePool.java +++ b/src/main/java/org/opensearch/security/auditlog/routing/AsyncStoragePool.java @@ -24,68 +24,75 @@ import org.opensearch.security.auditlog.sink.AuditLogSink; public class AsyncStoragePool { - private static final Logger log = LogManager.getLogger(AsyncStoragePool.class); - private final ExecutorService pool; - private final ThreadPoolConfig threadPoolConfig; + private static final Logger log = LogManager.getLogger(AsyncStoragePool.class); + private final ExecutorService pool; + private final ThreadPoolConfig threadPoolConfig; - public AsyncStoragePool(final ThreadPoolConfig threadPoolConfig) { - this.threadPoolConfig = threadPoolConfig; - this.pool = createExecutor(threadPoolConfig); - } + public AsyncStoragePool(final ThreadPoolConfig threadPoolConfig) { + this.threadPoolConfig = threadPoolConfig; + this.pool = createExecutor(threadPoolConfig); + } - public ThreadPoolConfig getConfig() { - return this.threadPoolConfig; - } + public ThreadPoolConfig getConfig() { + return this.threadPoolConfig; + } - public void submit(AuditMessage message, AuditLogSink sink) { - try { - pool.submit(() -> { - sink.store(message); - if (log.isTraceEnabled()) { - log.trace("stored on delegate {} asynchronously", sink.getClass().getSimpleName()); - } - }); - } catch (Exception ex) { - log.error("Could not submit audit message {} to thread pool for delegate '{}' due to '{}'", message, sink.getClass().getSimpleName(), ex.getMessage()); - if (sink.getFallbackSink() != null) { - sink.getFallbackSink().store(message); - } - } - } + public void submit(AuditMessage message, AuditLogSink sink) { + try { + pool.submit(() -> { + sink.store(message); + if (log.isTraceEnabled()) { + log.trace("stored on delegate {} asynchronously", sink.getClass().getSimpleName()); + } + }); + } catch (Exception ex) { + log.error( + "Could not submit audit message {} to thread pool for delegate '{}' due to '{}'", + message, + sink.getClass().getSimpleName(), + ex.getMessage() + ); + if (sink.getFallbackSink() != null) { + sink.getFallbackSink().store(message); + } + } + } - private static ThreadPoolExecutor createExecutor(final ThreadPoolConfig config) { - if (log.isDebugEnabled()) { - log.debug("Create new executor with threadPoolSize: {} and maxQueueLen: {}", - config.getThreadPoolSize(), - config.getThreadPoolMaxQueueLen()); - } - return new ThreadPoolExecutor( - config.getThreadPoolSize(), - config.getThreadPoolSize(), - 0L, - TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>(config.getThreadPoolMaxQueueLen())); - } + private static ThreadPoolExecutor createExecutor(final ThreadPoolConfig config) { + if (log.isDebugEnabled()) { + log.debug( + "Create new executor with threadPoolSize: {} and maxQueueLen: {}", + config.getThreadPoolSize(), + config.getThreadPoolMaxQueueLen() + ); + } + return new ThreadPoolExecutor( + config.getThreadPoolSize(), + config.getThreadPoolSize(), + 0L, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(config.getThreadPoolMaxQueueLen()) + ); + } - public void close() { + public void close() { - if (pool != null) { - pool.shutdown(); // Disable new tasks from being submitted + if (pool != null) { + pool.shutdown(); // Disable new tasks from being submitted - try { - // Wait a while for existing tasks to terminate - if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { - pool.shutdownNow(); // Cancel currently executing tasks - // Wait a while for tasks to respond to being cancelled - if (!pool.awaitTermination(60, TimeUnit.SECONDS)) - log.error("Pool did not terminate"); - } - } catch (InterruptedException ie) { - // (Re-)Cancel if current thread also interrupted - pool.shutdownNow(); - // Preserve interrupt status - Thread.currentThread().interrupt(); - } - } - } + try { + // Wait a while for existing tasks to terminate + if (!pool.awaitTermination(60, TimeUnit.SECONDS)) { + pool.shutdownNow(); // Cancel currently executing tasks + // Wait a while for tasks to respond to being cancelled + if (!pool.awaitTermination(60, TimeUnit.SECONDS)) log.error("Pool did not terminate"); + } + } catch (InterruptedException ie) { + // (Re-)Cancel if current thread also interrupted + pool.shutdownNow(); + // Preserve interrupt status + Thread.currentThread().interrupt(); + } + } + } } diff --git a/src/main/java/org/opensearch/security/auditlog/routing/AuditMessageRouter.java b/src/main/java/org/opensearch/security/auditlog/routing/AuditMessageRouter.java index a6a6f29f23..af120daf35 100644 --- a/src/main/java/org/opensearch/security/auditlog/routing/AuditMessageRouter.java +++ b/src/main/java/org/opensearch/security/auditlog/routing/AuditMessageRouter.java @@ -106,9 +106,12 @@ public final void enableRoutes(Settings settings) { if (categorySinks != null) { return; } - Map routesConfiguration = Utils.convertJsonToxToStructuredMap(settings.getAsSettings(ConfigConstants.SECURITY_AUDIT_CONFIG_ROUTES)); + Map routesConfiguration = Utils.convertJsonToxToStructuredMap( + settings.getAsSettings(ConfigConstants.SECURITY_AUDIT_CONFIG_ROUTES) + ); EnumSet presentAuditCategory = EnumSet.noneOf(AuditCategory.class); - categorySinks = routesConfiguration.entrySet().stream() + categorySinks = routesConfiguration.entrySet() + .stream() .peek(entry -> log.trace("Setting up routes for endpoint {}, configuration is {}", entry.getKey(), entry.getValue())) .map(entry -> { String categoryName = entry.getKey(); @@ -116,9 +119,16 @@ public final void enableRoutes(Settings settings) { // first set up all configured routes. We do it this way so category names are case insensitive // and we can warn if a non-existing category has been detected. AuditCategory auditCategory = AuditCategory.valueOf(categoryName.toUpperCase()); - return Maps.immutableEntry(auditCategory, createSinksForCategory(auditCategory, (Map>)entry.getValue())); + return Maps.immutableEntry( + auditCategory, + createSinksForCategory(auditCategory, (Map>) entry.getValue()) + ); } catch (IllegalArgumentException e) { - log.error("Invalid category '{}' found in routing configuration. Must be one of: {}", categoryName, AuditCategory.values()); + log.error( + "Invalid category '{}' found in routing configuration. Must be one of: {}", + categoryName, + AuditCategory.values() + ); return null; } }) @@ -138,12 +148,7 @@ public final void enableRoutes(Settings settings) { } return false; }) - .collect( - Maps.toImmutableEnumMap( - Map.Entry::getKey, - Map.Entry::getValue - ) - ); + .collect(Maps.toImmutableEnumMap(Map.Entry::getKey, Map.Entry::getValue)); // for all non-configured categories we automatically set up the default endpoint log.warn("No endpoint configured for categories {}, using default endpoint", EnumSet.complementOf(presentAuditCategory)); diff --git a/src/main/java/org/opensearch/security/auditlog/sink/AuditLogSink.java b/src/main/java/org/opensearch/security/auditlog/sink/AuditLogSink.java index 2f2c4da5b0..a482b81c29 100644 --- a/src/main/java/org/opensearch/security/auditlog/sink/AuditLogSink.java +++ b/src/main/java/org/opensearch/security/auditlog/sink/AuditLogSink.java @@ -38,7 +38,7 @@ public abstract class AuditLogSink { protected AuditLogSink(String name, Settings settings, String settingsPrefix, AuditLogSink fallbackSink) { this.name = name.toLowerCase(); - this.settings = Objects.requireNonNull(settings); + this.settings = Objects.requireNonNull(settings); this.settingsPrefix = settingsPrefix; this.fallbackSink = fallbackSink; @@ -51,34 +51,34 @@ public boolean isHandlingBackpressure() { } public String getName() { - return name; + return name; } public AuditLogSink getFallbackSink() { - return fallbackSink; + return fallbackSink; } public final void store(AuditMessage msg) { - if (!doStoreWithRetry(msg) && !fallbackSink.doStoreWithRetry(msg)) { - System.err.println(msg.toPrettyString()); - } + if (!doStoreWithRetry(msg) && !fallbackSink.doStoreWithRetry(msg)) { + System.err.println(msg.toPrettyString()); + } } private boolean doStoreWithRetry(AuditMessage msg) { - //retryCount of 0 means no retry (which is: try exactly once) - delayMs is ignored - //retryCount of 1 means: try and if this fails wait delayMs and try once again + // retryCount of 0 means no retry (which is: try exactly once) - delayMs is ignored + // retryCount of 1 means: try and if this fails wait delayMs and try once again - if(doStore(msg)) { + if (doStore(msg)) { return true; } final boolean isDebugEnabled = log.isDebugEnabled(); - for(int i=0; i DEFAULT_TLS_PROTOCOLS = Arrays.asList(new String[] { "TLSv1.2", "TLSv1.1"}); - // config in opensearch.yml - private final String index; - private final String type; - private final HttpClient client; - private List servers; - private DateTimeFormatter indexPattern; + private static final List DEFAULT_TLS_PROTOCOLS = Arrays.asList(new String[] { "TLSv1.2", "TLSv1.1" }); + // config in opensearch.yml + private final String index; + private final String type; + private final HttpClient client; + private List servers; + private DateTimeFormatter indexPattern; static final String PKCS12 = "PKCS12"; - public ExternalOpenSearchSink(final String name, final Settings settings, final String settingPrefix, final Path configPath, AuditLogSink fallbackSink) throws Exception { - - super(name, settings, settingPrefix, fallbackSink); - Settings sinkSettings = settings.getAsSettings(settingPrefix); - servers = sinkSettings.getAsList(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_HTTP_ENDPOINTS); - if (servers == null || servers.size() == 0) { - log.error("No http endpoints configured for external OpenSearch endpoint '{}', falling back to localhost.", name); - servers = Collections.singletonList("localhost:9200"); - } + public ExternalOpenSearchSink( + final String name, + final Settings settings, + final String settingPrefix, + final Path configPath, + AuditLogSink fallbackSink + ) throws Exception { + + super(name, settings, settingPrefix, fallbackSink); + Settings sinkSettings = settings.getAsSettings(settingPrefix); + servers = sinkSettings.getAsList(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_HTTP_ENDPOINTS); + if (servers == null || servers.size() == 0) { + log.error("No http endpoints configured for external OpenSearch endpoint '{}', falling back to localhost.", name); + servers = Collections.singletonList("localhost:9200"); + } - this.index = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_OPENSEARCH_INDEX, "'security-auditlog-'YYYY.MM.dd"); + this.index = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_OPENSEARCH_INDEX, "'security-auditlog-'YYYY.MM.dd"); - try { + try { this.indexPattern = DateTimeFormat.forPattern(index); } catch (IllegalArgumentException e) { - log.debug("Unable to parse index pattern due to {}. " - + "If you have no date pattern configured you can safely ignore this message", e.getMessage()); + log.debug( + "Unable to parse index pattern due to {}. " + "If you have no date pattern configured you can safely ignore this message", + e.getMessage() + ); } - this.type = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_OPENSEARCH_TYPE, null); - final boolean verifyHostnames = sinkSettings.getAsBoolean(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_VERIFY_HOSTNAMES, true); - final boolean enableSsl = sinkSettings.getAsBoolean(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL, false); - final boolean enableSslClientAuth = sinkSettings.getAsBoolean(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL_CLIENT_AUTH , ConfigConstants.OPENDISTRO_SECURITY_AUDIT_SSL_ENABLE_SSL_CLIENT_AUTH_DEFAULT); - final String user = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME); - final String password = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD); - - final HttpClientBuilder builder = HttpClient.builder(servers.toArray(new String[0])); - - if (enableSsl) { - - final boolean pem = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_FILEPATH, null) != null - || sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_CONTENT, null) != null; - - KeyStore effectiveTruststore; - KeyStore effectiveKeystore; - char[] effectiveKeyPassword; - String effectiveKeyAlias; - - final boolean isDebugEnabled = log.isDebugEnabled(); - - if(pem) { - X509Certificate[] trustCertificates = PemKeyReader.loadCertificatesFromStream(PemKeyReader.resolveStream(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_CONTENT, sinkSettings)); - - if(trustCertificates == null) { - String path = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_FILEPATH); - trustCertificates = PemKeyReader.loadCertificatesFromFile(PemKeyReader.resolve(path, ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_FILEPATH, settings, configPath, true)); + this.type = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_OPENSEARCH_TYPE, null); + final boolean verifyHostnames = sinkSettings.getAsBoolean( + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_VERIFY_HOSTNAMES, + true + ); + final boolean enableSsl = sinkSettings.getAsBoolean(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL, false); + final boolean enableSslClientAuth = sinkSettings.getAsBoolean( + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLE_SSL_CLIENT_AUTH, + ConfigConstants.OPENDISTRO_SECURITY_AUDIT_SSL_ENABLE_SSL_CLIENT_AUTH_DEFAULT + ); + final String user = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_USERNAME); + final String password = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PASSWORD); + + final HttpClientBuilder builder = HttpClient.builder(servers.toArray(new String[0])); + + if (enableSsl) { + + final boolean pem = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_FILEPATH, null) != null + || sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_CONTENT, null) != null; + + KeyStore effectiveTruststore; + KeyStore effectiveKeystore; + char[] effectiveKeyPassword; + String effectiveKeyAlias; + + final boolean isDebugEnabled = log.isDebugEnabled(); + + if (pem) { + X509Certificate[] trustCertificates = PemKeyReader.loadCertificatesFromStream( + PemKeyReader.resolveStream(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_CONTENT, sinkSettings) + ); + + if (trustCertificates == null) { + String path = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_FILEPATH); + trustCertificates = PemKeyReader.loadCertificatesFromFile( + PemKeyReader.resolve( + path, + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMTRUSTEDCAS_FILEPATH, + settings, + configPath, + true + ) + ); } - //for client authentication - X509Certificate[] authenticationCertificate = PemKeyReader.loadCertificatesFromStream(PemKeyReader.resolveStream(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_CONTENT, sinkSettings)); - - if(authenticationCertificate == null) { - String path = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_FILEPATH); - authenticationCertificate = PemKeyReader.loadCertificatesFromFile(PemKeyReader.resolve(path, ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_FILEPATH, settings, configPath, enableSslClientAuth)); + // for client authentication + X509Certificate[] authenticationCertificate = PemKeyReader.loadCertificatesFromStream( + PemKeyReader.resolveStream(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_CONTENT, sinkSettings) + ); + + if (authenticationCertificate == null) { + String path = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_FILEPATH); + authenticationCertificate = PemKeyReader.loadCertificatesFromFile( + PemKeyReader.resolve( + path, + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMCERT_FILEPATH, + settings, + configPath, + enableSslClientAuth + ) + ); } - PrivateKey authenticationKey = PemKeyReader.loadKeyFromStream(sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_PASSWORD), PemKeyReader.resolveStream(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_CONTENT, sinkSettings)); - - if(authenticationKey == null) { - String path = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_FILEPATH); - authenticationKey = PemKeyReader.loadKeyFromFile(sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_PASSWORD), PemKeyReader.resolve(path, ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_FILEPATH, settings, configPath, enableSslClientAuth)); + PrivateKey authenticationKey = PemKeyReader.loadKeyFromStream( + sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_PASSWORD), + PemKeyReader.resolveStream(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_CONTENT, sinkSettings) + ); + + if (authenticationKey == null) { + String path = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_FILEPATH); + authenticationKey = PemKeyReader.loadKeyFromFile( + sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_PASSWORD), + PemKeyReader.resolve( + path, + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_PEMKEY_FILEPATH, + settings, + configPath, + enableSslClientAuth + ) + ); } effectiveKeyPassword = PemKeyReader.randomChars(12); effectiveKeyAlias = "al"; effectiveTruststore = PemKeyReader.toTruststore(effectiveKeyAlias, trustCertificates); - effectiveKeystore = PemKeyReader.toKeystore(effectiveKeyAlias, effectiveKeyPassword, authenticationCertificate, authenticationKey); + effectiveKeystore = PemKeyReader.toKeystore( + effectiveKeyAlias, + effectiveKeyPassword, + authenticationCertificate, + authenticationKey + ); if (isDebugEnabled) { - log.debug("Use PEM to secure communication with auditlog server (client auth is {})", authenticationKey!=null); + log.debug("Use PEM to secure communication with auditlog server (client auth is {})", authenticationKey != null); } } else { - final KeyStore trustStore = PemKeyReader.loadKeyStore(PemKeyReader.resolve(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, settings, configPath, true) - , SECURITY_SSL_TRANSPORT_TRUSTSTORE_PASSWORD.getSetting(settings) - , settings.get(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_TYPE)); - - //for client authentication - final KeyStore keyStore = PemKeyReader.loadKeyStore(PemKeyReader.resolve(SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, settings, configPath, enableSslClientAuth) - , SECURITY_SSL_TRANSPORT_KEYSTORE_PASSWORD.getSetting(settings, SSLConfigConstants.DEFAULT_STORE_PASSWORD) - , settings.get(SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_TYPE)); - final String keyStorePassword = SECURITY_SSL_TRANSPORT_KEYSTORE_PASSWORD.getSetting(settings, SSLConfigConstants.DEFAULT_STORE_PASSWORD); - effectiveKeyPassword = keyStorePassword==null||keyStorePassword.isEmpty()?null:keyStorePassword.toCharArray(); + final KeyStore trustStore = PemKeyReader.loadKeyStore( + PemKeyReader.resolve(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, settings, configPath, true), + SECURITY_SSL_TRANSPORT_TRUSTSTORE_PASSWORD.getSetting(settings), + settings.get(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_TYPE) + ); + + // for client authentication + final KeyStore keyStore = PemKeyReader.loadKeyStore( + PemKeyReader.resolve( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_FILEPATH, + settings, + configPath, + enableSslClientAuth + ), + SECURITY_SSL_TRANSPORT_KEYSTORE_PASSWORD.getSetting(settings, SSLConfigConstants.DEFAULT_STORE_PASSWORD), + settings.get(SSLConfigConstants.SECURITY_SSL_TRANSPORT_KEYSTORE_TYPE) + ); + final String keyStorePassword = SECURITY_SSL_TRANSPORT_KEYSTORE_PASSWORD.getSetting( + settings, + SSLConfigConstants.DEFAULT_STORE_PASSWORD + ); + effectiveKeyPassword = keyStorePassword == null || keyStorePassword.isEmpty() ? null : keyStorePassword.toCharArray(); effectiveKeyAlias = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_JKS_CERT_ALIAS, null); - if(enableSslClientAuth && effectiveKeyAlias == null) { - throw new IllegalArgumentException(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_JKS_CERT_ALIAS+" not given"); + if (enableSslClientAuth && effectiveKeyAlias == null) { + throw new IllegalArgumentException(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_JKS_CERT_ALIAS + " not given"); } effectiveTruststore = trustStore; effectiveKeystore = keyStore; if (isDebugEnabled) { - log.debug("Use Trust-/Keystore to secure communication with LDAP server (client auth is {})", keyStore!=null); - log.debug("keyStoreAlias: {}", effectiveKeyAlias); + log.debug("Use Trust-/Keystore to secure communication with LDAP server (client auth is {})", keyStore != null); + log.debug("keyStoreAlias: {}", effectiveKeyAlias); } } - final List enabledCipherSuites = sinkSettings.getAsList(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_CIPHERS, null); - final List enabledProtocols = sinkSettings.getAsList(ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_PROTOCOLS, DEFAULT_TLS_PROTOCOLS); + final List enabledCipherSuites = sinkSettings.getAsList( + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_CIPHERS, + null + ); + final List enabledProtocols = sinkSettings.getAsList( + ConfigConstants.SECURITY_AUDIT_EXTERNAL_OPENSEARCH_ENABLED_SSL_PROTOCOLS, + DEFAULT_TLS_PROTOCOLS + ); - builder.setSupportedCipherSuites(enabledCipherSuites==null?null:enabledCipherSuites.toArray(new String[0])); + builder.setSupportedCipherSuites(enabledCipherSuites == null ? null : enabledCipherSuites.toArray(new String[0])); builder.setSupportedProtocols(enabledProtocols.toArray(new String[0])); - builder.enableSsl(effectiveTruststore, verifyHostnames); //trust all aliases + builder.enableSsl(effectiveTruststore, verifyHostnames); // trust all aliases if (enableSslClientAuth) { builder.setPkiCredentials(effectiveKeystore, effectiveKeyPassword, effectiveKeyAlias); } - } - - if (user != null && password != null) { - builder.setBasicCredentials(user, password); - } - - client = builder.build(); - } - - @Override - public void close() throws IOException { - if (client != null) { - client.close(); - } - } - - public boolean doStore(final AuditMessage msg) { - try { - boolean successful = client.index(msg.toString(), getExpandedIndexName(indexPattern, index), type, true); - if (!successful) { - log.error("Unable to send audit log {} to one of these servers: {}", msg, servers); - } - return successful; - } catch (Exception e) { - log.error("Unable to send audit log {} due to", msg, e); - return false; - } - } + } + + if (user != null && password != null) { + builder.setBasicCredentials(user, password); + } + + client = builder.build(); + } + + @Override + public void close() throws IOException { + if (client != null) { + client.close(); + } + } + + public boolean doStore(final AuditMessage msg) { + try { + boolean successful = client.index(msg.toString(), getExpandedIndexName(indexPattern, index), type, true); + if (!successful) { + log.error("Unable to send audit log {} to one of these servers: {}", msg, servers); + } + return successful; + } catch (Exception e) { + log.error("Unable to send audit log {} due to", msg, e); + return false; + } + } } diff --git a/src/main/java/org/opensearch/security/auditlog/sink/InternalOpenSearchSink.java b/src/main/java/org/opensearch/security/auditlog/sink/InternalOpenSearchSink.java index 9b7be332b6..4a4af8d1fe 100644 --- a/src/main/java/org/opensearch/security/auditlog/sink/InternalOpenSearchSink.java +++ b/src/main/java/org/opensearch/security/auditlog/sink/InternalOpenSearchSink.java @@ -30,53 +30,68 @@ public final class InternalOpenSearchSink extends AuditLogSink { - private final Client clientProvider; - final String index; - final String type; - private DateTimeFormatter indexPattern; - private final ThreadPool threadPool; + private final Client clientProvider; + final String index; + final String type; + private DateTimeFormatter indexPattern; + private final ThreadPool threadPool; - public InternalOpenSearchSink(final String name, final Settings settings, final String settingsPrefix, final Path configPath, final Client clientProvider, ThreadPool threadPool, AuditLogSink fallbackSink) { - super(name, settings, settingsPrefix, fallbackSink); - this.clientProvider = clientProvider; - Settings sinkSettings = getSinkSettings(settingsPrefix); + public InternalOpenSearchSink( + final String name, + final Settings settings, + final String settingsPrefix, + final Path configPath, + final Client clientProvider, + ThreadPool threadPool, + AuditLogSink fallbackSink + ) { + super(name, settings, settingsPrefix, fallbackSink); + this.clientProvider = clientProvider; + Settings sinkSettings = getSinkSettings(settingsPrefix); - this.index = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_OPENSEARCH_INDEX, "'security-auditlog-'YYYY.MM.dd"); - this.type = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_OPENSEARCH_TYPE, null); + this.index = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_OPENSEARCH_INDEX, "'security-auditlog-'YYYY.MM.dd"); + this.type = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_OPENSEARCH_TYPE, null); - this.threadPool = threadPool; - try { - this.indexPattern = DateTimeFormat.forPattern(index); - } catch (IllegalArgumentException e) { - log.debug("Unable to parse index pattern due to {}. " + "If you have no date pattern configured you can safely ignore this message", e.getMessage()); - } - } + this.threadPool = threadPool; + try { + this.indexPattern = DateTimeFormat.forPattern(index); + } catch (IllegalArgumentException e) { + log.debug( + "Unable to parse index pattern due to {}. " + "If you have no date pattern configured you can safely ignore this message", + e.getMessage() + ); + } + } - @Override - public void close() throws IOException { + @Override + public void close() throws IOException { - } + } - public boolean doStore(final AuditMessage msg) { + public boolean doStore(final AuditMessage msg) { - if (Boolean.parseBoolean((String) HeaderHelper.getSafeFromHeader(threadPool.getThreadContext(), ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER))) { - if (log.isTraceEnabled()) { - log.trace("audit log of audit log will not be executed"); - } - return true; - } + if (Boolean.parseBoolean( + (String) HeaderHelper.getSafeFromHeader(threadPool.getThreadContext(), ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER) + )) { + if (log.isTraceEnabled()) { + log.trace("audit log of audit log will not be executed"); + } + return true; + } - try (StoredContext ctx = threadPool.getThreadContext().stashContext()) { - try { - final IndexRequestBuilder irb = clientProvider.prepareIndex(getExpandedIndexName(indexPattern, index)).setRefreshPolicy(RefreshPolicy.IMMEDIATE).setSource(msg.getAsMap()); - threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); - irb.setTimeout(TimeValue.timeValueMinutes(1)); - irb.execute().actionGet(); - return true; - } catch (final Exception e) { - log.error("Unable to index audit log {} due to", msg, e); - return false; - } - } - } + try (StoredContext ctx = threadPool.getThreadContext().stashContext()) { + try { + final IndexRequestBuilder irb = clientProvider.prepareIndex(getExpandedIndexName(indexPattern, index)) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .setSource(msg.getAsMap()); + threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); + irb.setTimeout(TimeValue.timeValueMinutes(1)); + irb.execute().actionGet(); + return true; + } catch (final Exception e) { + log.error("Unable to index audit log {} due to", msg, e); + return false; + } + } + } } diff --git a/src/main/java/org/opensearch/security/auditlog/sink/KafkaSink.java b/src/main/java/org/opensearch/security/auditlog/sink/KafkaSink.java index a94a17ff03..e67ed66549 100644 --- a/src/main/java/org/opensearch/security/auditlog/sink/KafkaSink.java +++ b/src/main/java/org/opensearch/security/auditlog/sink/KafkaSink.java @@ -32,41 +32,41 @@ public class KafkaSink extends AuditLogSink { - private final String[] mandatoryProperties = new String []{"bootstrap_servers","topic_name"}; - private boolean valid = true; - private Producer producer; - private String topicName; + private final String[] mandatoryProperties = new String[] { "bootstrap_servers", "topic_name" }; + private boolean valid = true; + private Producer producer; + private String topicName; @SuppressWarnings("removal") - public KafkaSink(final String name, final Settings settings, final String settingsPrefix, AuditLogSink fallbackSink) { - super(name, settings, settingsPrefix, fallbackSink); + public KafkaSink(final String name, final Settings settings, final String settingsPrefix, AuditLogSink fallbackSink) { + super(name, settings, settingsPrefix, fallbackSink); - Settings sinkSettings = settings.getAsSettings(settingsPrefix); - checkMandatorySinkSettings(sinkSettings); + Settings sinkSettings = settings.getAsSettings(settingsPrefix); + checkMandatorySinkSettings(sinkSettings); - if (!valid) { - log.error("Failed to configure Kafka producer, please check the logfile."); - return; - } + if (!valid) { + log.error("Failed to configure Kafka producer, please check the logfile."); + return; + } final Properties producerProps = new Properties(); - for(String key: sinkSettings.names()) { - if(!key.equals("topic_name")) { + for (String key : sinkSettings.names()) { + if (!key.equals("topic_name")) { producerProps.put(key.replace('_', '.'), sinkSettings.get(key)); } } - producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, LongSerializer.class.getName()); - producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); - topicName = sinkSettings.get("topic_name"); + producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, LongSerializer.class.getName()); + producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); + topicName = sinkSettings.get("topic_name"); - //map path of - //ssl.keystore.location - //ssl.truststore.location - //sasl.kerberos.kinit.cmd + // map path of + // ssl.keystore.location + // ssl.truststore.location + // sasl.kerberos.kinit.cmd - final SecurityManager sm = System.getSecurityManager(); + final SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(new SpecialPermission()); @@ -84,49 +84,49 @@ public KafkaProducer run() throws Exception { this.valid = false; } - } + } - @Override - protected boolean doStore(AuditMessage msg) { - if (!valid || producer == null) { - return false; - } + @Override + protected boolean doStore(AuditMessage msg) { + if (!valid || producer == null) { + return false; + } - ProducerRecord data = new ProducerRecord(topicName, msg.toJson()); - producer.send(data, new Callback() { + ProducerRecord data = new ProducerRecord(topicName, msg.toJson()); + producer.send(data, new Callback() { @Override public void onCompletion(RecordMetadata metadata, Exception exception) { - if(exception == null) { - //log trace? - } else { - log.error("Could not store message on Kafka topic {}", topicName, exception); - fallbackSink.store(msg); - } + if (exception == null) { + // log trace? + } else { + log.error("Could not store message on Kafka topic {}", topicName, exception); + fallbackSink.store(msg); + } } }); - return true; - } - - @Override - public boolean isHandlingBackpressure() { - return true; - } - - private void checkMandatorySinkSettings(Settings sinkSettings) { - for(String mandatory: mandatoryProperties) { - String value = sinkSettings.get(mandatory); - if (value == null || value.length() == 0) { - log.error("No value for {} provided in configuration, this endpoint will not work.", value); - this.valid = false; - } - } - } + return true; + } + + @Override + public boolean isHandlingBackpressure() { + return true; + } + + private void checkMandatorySinkSettings(Settings sinkSettings) { + for (String mandatory : mandatoryProperties) { + String value = sinkSettings.get(mandatory); + if (value == null || value.length() == 0) { + log.error("No value for {} provided in configuration, this endpoint will not work.", value); + this.valid = false; + } + } + } @Override public void close() throws IOException { - if(producer != null) { + if (producer != null) { valid = false; producer.close(); } diff --git a/src/main/java/org/opensearch/security/auditlog/sink/Log4JSink.java b/src/main/java/org/opensearch/security/auditlog/sink/Log4JSink.java index 8794b90f41..f01043fa21 100644 --- a/src/main/java/org/opensearch/security/auditlog/sink/Log4JSink.java +++ b/src/main/java/org/opensearch/security/auditlog/sink/Log4JSink.java @@ -27,18 +27,18 @@ public final class Log4JSink extends AuditLogSink { public Log4JSink(final String name, final Settings settings, final String settingsPrefix, AuditLogSink fallbackSink) { super(name, settings, settingsPrefix, fallbackSink); - loggerName = settings.get( settingsPrefix + ".log4j.logger_name","sgaudit"); + loggerName = settings.get(settingsPrefix + ".log4j.logger_name", "sgaudit"); auditLogger = LogManager.getLogger(loggerName); - logLevel = Level.toLevel(settings.get(settingsPrefix + ".log4j.level","INFO").toUpperCase()); + logLevel = Level.toLevel(settings.get(settingsPrefix + ".log4j.level", "INFO").toUpperCase()); enabled = auditLogger.isEnabled(logLevel); } public boolean isHandlingBackpressure() { - return !enabled; //no submit to thread pool if not enabled + return !enabled; // no submit to thread pool if not enabled } public boolean doStore(final AuditMessage msg) { - if(enabled) { + if (enabled) { auditLogger.log(logLevel, msg.toJson()); } return true; diff --git a/src/main/java/org/opensearch/security/auditlog/sink/NoopSink.java b/src/main/java/org/opensearch/security/auditlog/sink/NoopSink.java index 740e7a4459..2981fa995d 100644 --- a/src/main/java/org/opensearch/security/auditlog/sink/NoopSink.java +++ b/src/main/java/org/opensearch/security/auditlog/sink/NoopSink.java @@ -27,7 +27,7 @@ public boolean isHandlingBackpressure() { @Override public boolean doStore(final AuditMessage msg) { - //do nothing + // do nothing return true; } diff --git a/src/main/java/org/opensearch/security/auditlog/sink/SinkProvider.java b/src/main/java/org/opensearch/security/auditlog/sink/SinkProvider.java index 270d106d2d..894c9162dd 100644 --- a/src/main/java/org/opensearch/security/auditlog/sink/SinkProvider.java +++ b/src/main/java/org/opensearch/security/auditlog/sink/SinkProvider.java @@ -27,147 +27,171 @@ public class SinkProvider { - protected final Logger log = LogManager.getLogger(this.getClass()); - private static final String FALLBACKSINK_NAME = "fallback"; - private static final String DEFAULTSINK_NAME = "default"; - private final Client clientProvider; - private final ThreadPool threadPool; - private final Path configPath; - private final Settings settings; - final Map allSinks = new HashMap<>(); - AuditLogSink defaultSink; - AuditLogSink fallbackSink; - - public SinkProvider(final Settings settings, final Client clientProvider, ThreadPool threadPool, final Path configPath) { - this.settings = settings; - this.clientProvider = clientProvider; - this.threadPool = threadPool; - this.configPath = configPath; - - // fall back sink, make sure we don't lose messages - String fallbackConfigPrefix = ConfigConstants.SECURITY_AUDIT_CONFIG_ENDPOINTS + "." + FALLBACKSINK_NAME; - Settings fallbackSinkSettings = settings.getAsSettings(fallbackConfigPrefix); - if(!fallbackSinkSettings.isEmpty()) { - this.fallbackSink = createSink(FALLBACKSINK_NAME, fallbackSinkSettings.get("type"), settings, fallbackConfigPrefix+".config"); - } - - // make sure we always have a fallback to write to - if (this.fallbackSink == null) { - this.fallbackSink = new DebugSink(FALLBACKSINK_NAME, settings, null); - } - - allSinks.put(FALLBACKSINK_NAME, this.fallbackSink); - - // create default sink - defaultSink = this.createSink(DEFAULTSINK_NAME, settings.get(ConfigConstants.SECURITY_AUDIT_TYPE_DEFAULT), settings, ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT); - if (defaultSink == null) { - log.error("Default endpoint could not be created, auditlog will not work properly."); - return; - } - - allSinks.put(DEFAULTSINK_NAME, defaultSink); - - // create all other sinks - Map sinkSettingsMap = Utils.convertJsonToxToStructuredMap(settings.getAsSettings(ConfigConstants.SECURITY_AUDIT_CONFIG_ENDPOINTS)); - - for (Entry sinkEntry : sinkSettingsMap.entrySet()) { - String sinkName = sinkEntry.getKey(); - // do not create fallback twice - if(sinkName.equalsIgnoreCase(FALLBACKSINK_NAME)) { - continue; - } - String type = settings.getAsSettings(ConfigConstants.SECURITY_AUDIT_CONFIG_ENDPOINTS + "." + sinkName).get("type"); - if (type == null) { - log.error("No type defined for endpoint {}.", sinkName); - continue; - } - AuditLogSink sink = createSink(sinkName, type, this.settings, ConfigConstants.SECURITY_AUDIT_CONFIG_ENDPOINTS + "." + sinkName + ".config"); - if (sink == null) { - log.error("Endpoint '{}' could not be created, check log file for further information.", sinkName); - continue; - } - allSinks.put(sinkName.toLowerCase(), sink); - if (log.isDebugEnabled()) { - log.debug("sink '{}' created successfully.", sinkName); - } - } - } - - public AuditLogSink getSink(String sinkName) { - return allSinks.get(sinkName.toLowerCase()); - } - - public AuditLogSink getDefaultSink() { - return defaultSink; - } - - public void close() { - for (AuditLogSink sink : allSinks.values()) { - close(sink); - } - } - - protected void close(AuditLogSink sink) { - try { - log.info("Closing {}", sink.getClass().getSimpleName()); - sink.close(); - } catch (Exception ex) { - log.info("Could not close sink '{}' due to '{}'", sink.getClass().getSimpleName(), ex.getMessage()); - } - } - - private final AuditLogSink createSink(final String name, final String type, final Settings settings, final String settingsPrefix) { - AuditLogSink sink = null; - if (type != null) { - switch (type.toLowerCase()) { - case "internal_opensearch": - sink = new InternalOpenSearchSink(name, settings, settingsPrefix, configPath, clientProvider, threadPool, fallbackSink); - break; - case "external_opensearch": - try { - sink = new ExternalOpenSearchSink(name, settings, settingsPrefix, configPath, fallbackSink); - } catch (Exception e) { - log.error("Audit logging unavailable: Unable to setup HttpOpenSearchAuditLog due to", e); - } - break; - case "webhook": - try { - sink = new WebhookSink(name, settings, settingsPrefix, configPath, fallbackSink); - } catch (Exception e1) { - log.error("Audit logging unavailable: Unable to setup WebhookAuditLog due to", e1); - } - break; - case "debug": - sink = new DebugSink(name, settings, fallbackSink); - break; - case "noop": - sink = new NoopSink(name, settings, fallbackSink); - break; - case "log4j": - sink = new Log4JSink(name, settings, settingsPrefix, fallbackSink); - break; - case "kafka": - sink = new KafkaSink(name, settings, settingsPrefix, fallbackSink); - break; - default: - try { - Class delegateClass = Class.forName(type); - if (AuditLogSink.class.isAssignableFrom(delegateClass)) { - try { - sink = (AuditLogSink) delegateClass.getConstructor(String.class, Settings.class, String.class, Path.class, Client.class, ThreadPool.class, AuditLogSink.class).newInstance(name, settings, settingsPrefix, configPath, - clientProvider, threadPool, fallbackSink); - } catch (Throwable e) { - sink = (AuditLogSink) delegateClass.getConstructor(String.class, Settings.class, String.class, AuditLogSink.class).newInstance(name, settings, settingsPrefix, fallbackSink); - } - } else { - log.error("Audit logging unavailable: '{}' is not a subclass of {}", type, AuditLogSink.class.getSimpleName()); - } - } catch (Throwable e) { // we need really catch a Throwable here! - log.error("Audit logging unavailable: Cannot instantiate object of class {} due to ", type, e); - } - } - } - return sink; - } + protected final Logger log = LogManager.getLogger(this.getClass()); + private static final String FALLBACKSINK_NAME = "fallback"; + private static final String DEFAULTSINK_NAME = "default"; + private final Client clientProvider; + private final ThreadPool threadPool; + private final Path configPath; + private final Settings settings; + final Map allSinks = new HashMap<>(); + AuditLogSink defaultSink; + AuditLogSink fallbackSink; + + public SinkProvider(final Settings settings, final Client clientProvider, ThreadPool threadPool, final Path configPath) { + this.settings = settings; + this.clientProvider = clientProvider; + this.threadPool = threadPool; + this.configPath = configPath; + + // fall back sink, make sure we don't lose messages + String fallbackConfigPrefix = ConfigConstants.SECURITY_AUDIT_CONFIG_ENDPOINTS + "." + FALLBACKSINK_NAME; + Settings fallbackSinkSettings = settings.getAsSettings(fallbackConfigPrefix); + if (!fallbackSinkSettings.isEmpty()) { + this.fallbackSink = createSink(FALLBACKSINK_NAME, fallbackSinkSettings.get("type"), settings, fallbackConfigPrefix + ".config"); + } + + // make sure we always have a fallback to write to + if (this.fallbackSink == null) { + this.fallbackSink = new DebugSink(FALLBACKSINK_NAME, settings, null); + } + + allSinks.put(FALLBACKSINK_NAME, this.fallbackSink); + + // create default sink + defaultSink = this.createSink( + DEFAULTSINK_NAME, + settings.get(ConfigConstants.SECURITY_AUDIT_TYPE_DEFAULT), + settings, + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT + ); + if (defaultSink == null) { + log.error("Default endpoint could not be created, auditlog will not work properly."); + return; + } + + allSinks.put(DEFAULTSINK_NAME, defaultSink); + + // create all other sinks + Map sinkSettingsMap = Utils.convertJsonToxToStructuredMap( + settings.getAsSettings(ConfigConstants.SECURITY_AUDIT_CONFIG_ENDPOINTS) + ); + + for (Entry sinkEntry : sinkSettingsMap.entrySet()) { + String sinkName = sinkEntry.getKey(); + // do not create fallback twice + if (sinkName.equalsIgnoreCase(FALLBACKSINK_NAME)) { + continue; + } + String type = settings.getAsSettings(ConfigConstants.SECURITY_AUDIT_CONFIG_ENDPOINTS + "." + sinkName).get("type"); + if (type == null) { + log.error("No type defined for endpoint {}.", sinkName); + continue; + } + AuditLogSink sink = createSink( + sinkName, + type, + this.settings, + ConfigConstants.SECURITY_AUDIT_CONFIG_ENDPOINTS + "." + sinkName + ".config" + ); + if (sink == null) { + log.error("Endpoint '{}' could not be created, check log file for further information.", sinkName); + continue; + } + allSinks.put(sinkName.toLowerCase(), sink); + if (log.isDebugEnabled()) { + log.debug("sink '{}' created successfully.", sinkName); + } + } + } + + public AuditLogSink getSink(String sinkName) { + return allSinks.get(sinkName.toLowerCase()); + } + + public AuditLogSink getDefaultSink() { + return defaultSink; + } + + public void close() { + for (AuditLogSink sink : allSinks.values()) { + close(sink); + } + } + + protected void close(AuditLogSink sink) { + try { + log.info("Closing {}", sink.getClass().getSimpleName()); + sink.close(); + } catch (Exception ex) { + log.info("Could not close sink '{}' due to '{}'", sink.getClass().getSimpleName(), ex.getMessage()); + } + } + + private final AuditLogSink createSink(final String name, final String type, final Settings settings, final String settingsPrefix) { + AuditLogSink sink = null; + if (type != null) { + switch (type.toLowerCase()) { + case "internal_opensearch": + sink = new InternalOpenSearchSink(name, settings, settingsPrefix, configPath, clientProvider, threadPool, fallbackSink); + break; + case "external_opensearch": + try { + sink = new ExternalOpenSearchSink(name, settings, settingsPrefix, configPath, fallbackSink); + } catch (Exception e) { + log.error("Audit logging unavailable: Unable to setup HttpOpenSearchAuditLog due to", e); + } + break; + case "webhook": + try { + sink = new WebhookSink(name, settings, settingsPrefix, configPath, fallbackSink); + } catch (Exception e1) { + log.error("Audit logging unavailable: Unable to setup WebhookAuditLog due to", e1); + } + break; + case "debug": + sink = new DebugSink(name, settings, fallbackSink); + break; + case "noop": + sink = new NoopSink(name, settings, fallbackSink); + break; + case "log4j": + sink = new Log4JSink(name, settings, settingsPrefix, fallbackSink); + break; + case "kafka": + sink = new KafkaSink(name, settings, settingsPrefix, fallbackSink); + break; + default: + try { + Class delegateClass = Class.forName(type); + if (AuditLogSink.class.isAssignableFrom(delegateClass)) { + try { + sink = (AuditLogSink) delegateClass.getConstructor( + String.class, + Settings.class, + String.class, + Path.class, + Client.class, + ThreadPool.class, + AuditLogSink.class + ).newInstance(name, settings, settingsPrefix, configPath, clientProvider, threadPool, fallbackSink); + } catch (Throwable e) { + sink = (AuditLogSink) delegateClass.getConstructor( + String.class, + Settings.class, + String.class, + AuditLogSink.class + ).newInstance(name, settings, settingsPrefix, fallbackSink); + } + } else { + log.error("Audit logging unavailable: '{}' is not a subclass of {}", type, AuditLogSink.class.getSimpleName()); + } + } catch (Throwable e) { // we need really catch a Throwable here! + log.error("Audit logging unavailable: Cannot instantiate object of class {} due to ", type, e); + } + } + } + return sink; + } } diff --git a/src/main/java/org/opensearch/security/auditlog/sink/WebhookSink.java b/src/main/java/org/opensearch/security/auditlog/sink/WebhookSink.java index 083df01bbd..78780fb8e1 100644 --- a/src/main/java/org/opensearch/security/auditlog/sink/WebhookSink.java +++ b/src/main/java/org/opensearch/security/auditlog/sink/WebhookSink.java @@ -54,301 +54,320 @@ public class WebhookSink extends AuditLogSink { - /* HttpClient is thread safe */ - private final CloseableHttpClient httpClient; - - String webhookUrl = null; - WebhookFormat webhookFormat = null; - final boolean verifySSL; - final KeyStore effectiveTruststore; - - public WebhookSink(final String name, final Settings settings, final String settingsPrefix, final Path configPath, AuditLogSink fallbackSink) throws Exception { - super(name, settings, settingsPrefix, fallbackSink); - - Settings sinkSettings = settings.getAsSettings(settingsPrefix); - - this.effectiveTruststore = getEffectiveKeyStore(configPath); - - final String webhookUrl = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_WEBHOOK_URL); - final String format = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_WEBHOOK_FORMAT); - - verifySSL = sinkSettings.getAsBoolean(ConfigConstants.SECURITY_AUDIT_WEBHOOK_SSL_VERIFY, true); - httpClient = getHttpClient(); - - if(httpClient == null) { - log.error("Could not create HttpClient, audit log not available."); - return; - } - - if (Strings.isEmpty(webhookUrl)) { - log.error("plugins.security.audit.config.webhook.url not provided, webhook audit log will not work"); - return; - } else { - try { - // Sanity - check URL validity - new URL(webhookUrl); - this.webhookUrl = webhookUrl; - } catch (MalformedURLException ex) { - log.error("URL {} is invalid, webhook audit log will not work.", webhookUrl, ex); - } - } - - if (Strings.isEmpty(format)) { - log.warn("plugins.security.audit.config.webhook.format not provided, falling back to 'text'"); - webhookFormat = WebhookFormat.TEXT; - } else { - try { - webhookFormat = WebhookFormat.valueOf(format.toUpperCase()); - } catch (Exception ex) { - log.error("Could not find WebhookFormat for type {}, falling back to 'text'", format, ex); - webhookFormat = WebhookFormat.TEXT; - } - } - } - - @Override + /* HttpClient is thread safe */ + private final CloseableHttpClient httpClient; + + String webhookUrl = null; + WebhookFormat webhookFormat = null; + final boolean verifySSL; + final KeyStore effectiveTruststore; + + public WebhookSink( + final String name, + final Settings settings, + final String settingsPrefix, + final Path configPath, + AuditLogSink fallbackSink + ) throws Exception { + super(name, settings, settingsPrefix, fallbackSink); + + Settings sinkSettings = settings.getAsSettings(settingsPrefix); + + this.effectiveTruststore = getEffectiveKeyStore(configPath); + + final String webhookUrl = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_WEBHOOK_URL); + final String format = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_WEBHOOK_FORMAT); + + verifySSL = sinkSettings.getAsBoolean(ConfigConstants.SECURITY_AUDIT_WEBHOOK_SSL_VERIFY, true); + httpClient = getHttpClient(); + + if (httpClient == null) { + log.error("Could not create HttpClient, audit log not available."); + return; + } + + if (Strings.isEmpty(webhookUrl)) { + log.error("plugins.security.audit.config.webhook.url not provided, webhook audit log will not work"); + return; + } else { + try { + // Sanity - check URL validity + new URL(webhookUrl); + this.webhookUrl = webhookUrl; + } catch (MalformedURLException ex) { + log.error("URL {} is invalid, webhook audit log will not work.", webhookUrl, ex); + } + } + + if (Strings.isEmpty(format)) { + log.warn("plugins.security.audit.config.webhook.format not provided, falling back to 'text'"); + webhookFormat = WebhookFormat.TEXT; + } else { + try { + webhookFormat = WebhookFormat.valueOf(format.toUpperCase()); + } catch (Exception ex) { + log.error("Could not find WebhookFormat for type {}, falling back to 'text'", format, ex); + webhookFormat = WebhookFormat.TEXT; + } + } + } + + @Override @SuppressWarnings("removal") - public boolean doStore(AuditMessage msg) { - if (Strings.isEmpty(webhookUrl)) { - log.debug("Webhook URL is null"); - return false; - } - if (msg == null) { - log.debug("Message is null"); - return true; - } - - return AccessController.doPrivileged(new PrivilegedAction() { - - @Override - public Boolean run() { - boolean success = false; - try { - switch (webhookFormat.method) { - case POST: - success = post(msg); - break; - case GET: - success =get(msg); - break; - default: - log.error("Http Method '{}' defined in WebhookFormat '{}' not implemented yet", webhookFormat.method.name(), - webhookFormat.name()); - } - // log something in case endpoint is not reachable or did not return 200 - if (!success) { - log.error(msg.toString()); - } - return success; - } catch(Throwable t) { - log.error("Uncaught exception while trying to log message.", t); - log.error(msg.toString()); - return false; - } - } - }); - } + public boolean doStore(AuditMessage msg) { + if (Strings.isEmpty(webhookUrl)) { + log.debug("Webhook URL is null"); + return false; + } + if (msg == null) { + log.debug("Message is null"); + return true; + } + + return AccessController.doPrivileged(new PrivilegedAction() { + + @Override + public Boolean run() { + boolean success = false; + try { + switch (webhookFormat.method) { + case POST: + success = post(msg); + break; + case GET: + success = get(msg); + break; + default: + log.error( + "Http Method '{}' defined in WebhookFormat '{}' not implemented yet", + webhookFormat.method.name(), + webhookFormat.name() + ); + } + // log something in case endpoint is not reachable or did not return 200 + if (!success) { + log.error(msg.toString()); + } + return success; + } catch (Throwable t) { + log.error("Uncaught exception while trying to log message.", t); + log.error(msg.toString()); + return false; + } + } + }); + } @Override public void close() throws IOException { - if(httpClient != null) { - httpClient.close(); + if (httpClient != null) { + httpClient.close(); } } + /** + * Transforms an {@link AuditMessage} to JSON. By default, all fields are + * included in the JSON string. This method can be overridden by subclasses + * if a specific JSON format is needed. + * + * @param msg the AuditMessage to transform + * @return the JSON string + */ + protected String formatJson(final AuditMessage msg) { + return msg.toJson(); + } - /** - * Transforms an {@link AuditMessage} to JSON. By default, all fields are - * included in the JSON string. This method can be overridden by subclasses - * if a specific JSON format is needed. - * - * @param msg the AuditMessage to transform - * @return the JSON string - */ - protected String formatJson(final AuditMessage msg) { - return msg.toJson(); - } - - /** - * Transforms an {@link AuditMessage} to plain text. This method can be overridden - * by subclasses if a specific text format is needed. - * - * @param msg the AuditMessage to transform - * @return the text string - */ - protected String formatText(AuditMessage msg) { - return msg.toText(); - } - - /** - * Transforms an {@link AuditMessage} to Slack format. - * The default implementation returns - *

-	 * {
-	 *   "text": ""
-	 * }
-	 * 
- *

- * Can be overridden by subclasses if a more specific format is needed. - * - * @param msg the AuditMessage to transform - * @return the Slack formatted JSON string - */ - protected String formatSlack(AuditMessage msg) { - return "{\"text\": \"" + msg.toText() + "\"}"; - } - - /** - * Transforms an {@link AuditMessage} to a query parameter String. - * Used by {@link WebhookFormat#URL_PARAMETER_GET} and - * Used by {@link WebhookFormat#URL_PARAMETER_POST}. Can be overridden by - * subclasses if a specific format is needed. - * - * @param msg the AuditMessage to transform - * @return the query parameter string - */ - protected String formatUrlParameters(AuditMessage msg) { - return msg.toUrlParameters(); - } - - boolean get(AuditMessage msg) { - switch (webhookFormat) { - case URL_PARAMETER_GET: - return doGet(webhookUrl + formatUrlParameters(msg)); - default: - log.error("WebhookFormat '{}' not implemented yet", webhookFormat.name()); - return false; - } - } - - protected boolean doGet(String url) { - HttpGet httpGet = new HttpGet(url); - CloseableHttpResponse serverResponse = null; - try { - serverResponse = httpClient.execute(httpGet); - int responseCode = serverResponse.getCode(); - if (responseCode != HttpStatus.SC_OK) { - log.error("Cannot GET to webhook URL '{}', server returned status {}", webhookUrl, responseCode); - return false; - } - return true; - } catch (Throwable e) { - log.error("Cannot GET to webhook URL '{}'", webhookUrl, e); - return false; - } finally { - try { - if (serverResponse != null) { - serverResponse.close(); - } - } catch (IOException e) { - log.error("Cannot close server response", e); - } - } - } - - boolean post(AuditMessage msg) { - - String payload; - String url = webhookUrl; - - switch (webhookFormat) { - case JSON: - payload = formatJson(msg); - break; - case TEXT: - payload = formatText(msg); - break; - case SLACK: - payload = "{\"text\": \"" + msg.toText() + "\"}"; - break; - case URL_PARAMETER_POST: - payload = ""; - url = webhookUrl + formatUrlParameters(msg); - break; - default: - log.error("WebhookFormat '{}' not implemented yet", webhookFormat.name()); - return false; - } - - return doPost(url, payload); - - } - - protected boolean doPost(String url, String payload) { - - HttpPost postRequest = new HttpPost(url); - - StringEntity input = new StringEntity(payload, webhookFormat.contentType.withCharset(StandardCharsets.UTF_8)); - postRequest.setEntity(input); - - CloseableHttpResponse serverResponse = null; - try { - serverResponse = httpClient.execute(postRequest); - int responseCode = serverResponse.getCode(); - if (responseCode != HttpStatus.SC_OK) { - log.error("Cannot POST to webhook URL '{}', server returned status {}", webhookUrl, responseCode); - return false; - } - return true; - } catch (Throwable e) { - log.error("Cannot POST to webhook URL '{}' due to '{}'", webhookUrl, e.getMessage(), e); - return false; - } finally { - try { - if (serverResponse != null) { - serverResponse.close(); - } - } catch (IOException e) { - log.error("Cannot close server response", e); - } - } - } + /** + * Transforms an {@link AuditMessage} to plain text. This method can be overridden + * by subclasses if a specific text format is needed. + * + * @param msg the AuditMessage to transform + * @return the text string + */ + protected String formatText(AuditMessage msg) { + return msg.toText(); + } - @SuppressWarnings("removal") - private KeyStore getEffectiveKeyStore(final Path configPath) { + /** + * Transforms an {@link AuditMessage} to Slack format. + * The default implementation returns + *

+     * {
+     *   "text": ""
+     * }
+     * 
+ *

+ * Can be overridden by subclasses if a more specific format is needed. + * + * @param msg the AuditMessage to transform + * @return the Slack formatted JSON string + */ + protected String formatSlack(AuditMessage msg) { + return "{\"text\": \"" + msg.toText() + "\"}"; + } - return AccessController.doPrivileged(new PrivilegedAction() { + /** + * Transforms an {@link AuditMessage} to a query parameter String. + * Used by {@link WebhookFormat#URL_PARAMETER_GET} and + * Used by {@link WebhookFormat#URL_PARAMETER_POST}. Can be overridden by + * subclasses if a specific format is needed. + * + * @param msg the AuditMessage to transform + * @return the query parameter string + */ + protected String formatUrlParameters(AuditMessage msg) { + return msg.toUrlParameters(); + } - @Override - public KeyStore run() { - try { - Settings sinkSettings = settings.getAsSettings(settingsPrefix); + boolean get(AuditMessage msg) { + switch (webhookFormat) { + case URL_PARAMETER_GET: + return doGet(webhookUrl + formatUrlParameters(msg)); + default: + log.error("WebhookFormat '{}' not implemented yet", webhookFormat.name()); + return false; + } + } - final boolean pem = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_FILEPATH, null) != null - || sinkSettings.get(ConfigConstants.SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_CONTENT, null) != null; + protected boolean doGet(String url) { + HttpGet httpGet = new HttpGet(url); + CloseableHttpResponse serverResponse = null; + try { + serverResponse = httpClient.execute(httpGet); + int responseCode = serverResponse.getCode(); + if (responseCode != HttpStatus.SC_OK) { + log.error("Cannot GET to webhook URL '{}', server returned status {}", webhookUrl, responseCode); + return false; + } + return true; + } catch (Throwable e) { + log.error("Cannot GET to webhook URL '{}'", webhookUrl, e); + return false; + } finally { + try { + if (serverResponse != null) { + serverResponse.close(); + } + } catch (IOException e) { + log.error("Cannot close server response", e); + } + } + } - if(pem) { - X509Certificate[] trustCertificates = PemKeyReader.loadCertificatesFromStream(PemKeyReader.resolveStream(ConfigConstants.SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_CONTENT, sinkSettings)); + boolean post(AuditMessage msg) { + + String payload; + String url = webhookUrl; + + switch (webhookFormat) { + case JSON: + payload = formatJson(msg); + break; + case TEXT: + payload = formatText(msg); + break; + case SLACK: + payload = "{\"text\": \"" + msg.toText() + "\"}"; + break; + case URL_PARAMETER_POST: + payload = ""; + url = webhookUrl + formatUrlParameters(msg); + break; + default: + log.error("WebhookFormat '{}' not implemented yet", webhookFormat.name()); + return false; + } + + return doPost(url, payload); - if(trustCertificates == null) { - String fullPath = settingsPrefix + "." + ConfigConstants.SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_FILEPATH; - trustCertificates = PemKeyReader.loadCertificatesFromFile(PemKeyReader.resolve(fullPath, settings, configPath, false)); - } + } - return PemKeyReader.toTruststore("alw", trustCertificates); + protected boolean doPost(String url, String payload) { + HttpPost postRequest = new HttpPost(url); - } else { - return PemKeyReader.loadKeyStore(PemKeyReader.resolve(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, settings, configPath, false) - , SECURITY_SSL_TRANSPORT_TRUSTSTORE_PASSWORD.getSetting(settings) - , settings.get(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_TYPE)); - } - } catch(Exception ex) { - log.error("Could not load key material. Make sure your certificates are located relative to the config directory", ex); - return null; - } - } - }); - } + StringEntity input = new StringEntity(payload, webhookFormat.contentType.withCharset(StandardCharsets.UTF_8)); + postRequest.setEntity(input); - CloseableHttpClient getHttpClient() { + CloseableHttpResponse serverResponse = null; + try { + serverResponse = httpClient.execute(postRequest); + int responseCode = serverResponse.getCode(); + if (responseCode != HttpStatus.SC_OK) { + log.error("Cannot POST to webhook URL '{}', server returned status {}", webhookUrl, responseCode); + return false; + } + return true; + } catch (Throwable e) { + log.error("Cannot POST to webhook URL '{}' due to '{}'", webhookUrl, e.getMessage(), e); + return false; + } finally { + try { + if (serverResponse != null) { + serverResponse.close(); + } + } catch (IOException e) { + log.error("Cannot close server response", e); + } + } + } + + @SuppressWarnings("removal") + private KeyStore getEffectiveKeyStore(final Path configPath) { + + return AccessController.doPrivileged(new PrivilegedAction() { + + @Override + public KeyStore run() { + try { + Settings sinkSettings = settings.getAsSettings(settingsPrefix); + + final boolean pem = sinkSettings.get(ConfigConstants.SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_FILEPATH, null) != null + || sinkSettings.get(ConfigConstants.SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_CONTENT, null) != null; + + if (pem) { + X509Certificate[] trustCertificates = PemKeyReader.loadCertificatesFromStream( + PemKeyReader.resolveStream(ConfigConstants.SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_CONTENT, sinkSettings) + ); + + if (trustCertificates == null) { + String fullPath = settingsPrefix + "." + ConfigConstants.SECURITY_AUDIT_WEBHOOK_PEMTRUSTEDCAS_FILEPATH; + trustCertificates = PemKeyReader.loadCertificatesFromFile( + PemKeyReader.resolve(fullPath, settings, configPath, false) + ); + } + + return PemKeyReader.toTruststore("alw", trustCertificates); + + } else { + return PemKeyReader.loadKeyStore( + PemKeyReader.resolve( + SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_FILEPATH, + settings, + configPath, + false + ), + SECURITY_SSL_TRANSPORT_TRUSTSTORE_PASSWORD.getSetting(settings), + settings.get(SSLConfigConstants.SECURITY_SSL_TRANSPORT_TRUSTSTORE_TYPE) + ); + } + } catch (Exception ex) { + log.error("Could not load key material. Make sure your certificates are located relative to the config directory", ex); + return null; + } + } + }); + } + + CloseableHttpClient getHttpClient() { // TODO: set a timeout until we have a proper way to deal with back pressure int timeout = 5; RequestConfig config = RequestConfig.custom() - .setConnectTimeout(timeout, TimeUnit.SECONDS) - .setConnectionRequestTimeout(timeout, TimeUnit.SECONDS).build(); + .setConnectTimeout(timeout, TimeUnit.SECONDS) + .setConnectionRequestTimeout(timeout, TimeUnit.SECONDS) + .build(); final TrustStrategy trustAllStrategy = new TrustStrategy() { @Override @@ -357,76 +376,74 @@ public boolean isTrusted(X509Certificate[] chain, String authType) { } }; - try { - - HttpClientBuilder hcb = HttpClients.custom().setDefaultRequestConfig(config); - if(!verifySSL) { - SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(trustAllStrategy).build(); - final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, null, null, - NoopHostnameVerifier.INSTANCE); - - final HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create() - .setSSLSocketFactory(sslsf) - .setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(timeout, TimeUnit.SECONDS).build()) - .build(); - hcb.setConnectionManager(cm); - return hcb.build(); - } - - if(effectiveTruststore == null) { - return HttpClients.custom() - .setDefaultRequestConfig(config) - .build(); - } - SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(effectiveTruststore, null).build(); - final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, null, null, - new DefaultHostnameVerifier()); - - final HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create() - .setSSLSocketFactory(sslsf) - .setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(timeout, TimeUnit.SECONDS).build()) - .build(); - hcb.setConnectionManager(cm); - - return hcb.build(); - - - } catch(Exception ex) { - log.error("Could not create HTTPClient due to {}, audit log not available.", ex.getMessage(), ex); - return null; - } - } - - public static enum WebhookFormat { - URL_PARAMETER_GET(HttpMethod.GET, ContentType.TEXT_PLAIN), - URL_PARAMETER_POST(HttpMethod.POST, ContentType.TEXT_PLAIN), - TEXT(HttpMethod.POST, ContentType.TEXT_PLAIN), - JSON(HttpMethod.POST, ContentType.APPLICATION_JSON), - SLACK(HttpMethod.POST, ContentType.APPLICATION_JSON); - - private HttpMethod method; - private ContentType contentType; - - private WebhookFormat(HttpMethod method, ContentType contentType) { - this.method = method; - this.contentType = contentType; - } - - HttpMethod getMethod() { - return method; - } - - ContentType getContentType() { - return contentType; - } - - - } - - private static enum HttpMethod { - GET, - POST; - } + try { + + HttpClientBuilder hcb = HttpClients.custom().setDefaultRequestConfig(config); + if (!verifySSL) { + SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(trustAllStrategy).build(); + final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory( + sslContext, + null, + null, + NoopHostnameVerifier.INSTANCE + ); + + final HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create() + .setSSLSocketFactory(sslsf) + .setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(timeout, TimeUnit.SECONDS).build()) + .build(); + hcb.setConnectionManager(cm); + return hcb.build(); + } + + if (effectiveTruststore == null) { + return HttpClients.custom().setDefaultRequestConfig(config).build(); + } + SSLContext sslContext = SSLContextBuilder.create().loadTrustMaterial(effectiveTruststore, null).build(); + final SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, null, null, new DefaultHostnameVerifier()); + + final HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create() + .setSSLSocketFactory(sslsf) + .setDefaultSocketConfig(SocketConfig.custom().setSoTimeout(timeout, TimeUnit.SECONDS).build()) + .build(); + hcb.setConnectionManager(cm); + + return hcb.build(); + } catch (Exception ex) { + log.error("Could not create HTTPClient due to {}, audit log not available.", ex.getMessage(), ex); + return null; + } + } + + public static enum WebhookFormat { + URL_PARAMETER_GET(HttpMethod.GET, ContentType.TEXT_PLAIN), + URL_PARAMETER_POST(HttpMethod.POST, ContentType.TEXT_PLAIN), + TEXT(HttpMethod.POST, ContentType.TEXT_PLAIN), + JSON(HttpMethod.POST, ContentType.APPLICATION_JSON), + SLACK(HttpMethod.POST, ContentType.APPLICATION_JSON); + + private HttpMethod method; + private ContentType contentType; + + private WebhookFormat(HttpMethod method, ContentType contentType) { + this.method = method; + this.contentType = contentType; + } + + HttpMethod getMethod() { + return method; + } + + ContentType getContentType() { + return contentType; + } + + } + + private static enum HttpMethod { + GET, + POST; + } } diff --git a/src/main/java/org/opensearch/security/auth/AuthDomain.java b/src/main/java/org/opensearch/security/auth/AuthDomain.java index effa29adf3..decad5f068 100644 --- a/src/main/java/org/opensearch/security/auth/AuthDomain.java +++ b/src/main/java/org/opensearch/security/auth/AuthDomain.java @@ -61,8 +61,15 @@ public int getOrder() { @Override public String toString() { - return "AuthDomain [backend=" + backend + ", httpAuthenticator=" + httpAuthenticator + ", order=" + order + ", challenge=" - + challenge + "]"; + return "AuthDomain [backend=" + + backend + + ", httpAuthenticator=" + + httpAuthenticator + + ", order=" + + order + + ", challenge=" + + challenge + + "]"; } @Override diff --git a/src/main/java/org/opensearch/security/auth/BackendRegistry.java b/src/main/java/org/opensearch/security/auth/BackendRegistry.java index 9e8154230a..0a287d19f5 100644 --- a/src/main/java/org/opensearch/security/auth/BackendRegistry.java +++ b/src/main/java/org/opensearch/security/auth/BackendRegistry.java @@ -26,7 +26,6 @@ package org.opensearch.security.auth; - import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.Collection; @@ -85,44 +84,55 @@ public class BackendRegistry { private final XFFResolver xffResolver; private volatile boolean anonymousAuthEnabled = false; private final Settings opensearchSettings; - //private final InternalAuthenticationBackend iab; + // private final InternalAuthenticationBackend iab; private final AuditLog auditLog; private final ThreadPool threadPool; private final UserInjector userInjector; private final int ttlInMin; - private Cache userCache; //rest standard - private Cache restImpersonationCache; //used for rest impersonation + private Cache userCache; // rest standard + private Cache restImpersonationCache; // used for rest impersonation private Cache> restRoleCache; // 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(); + 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(); - 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(); + 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(); - restRoleCache = 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(); + restRoleCache = 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 AdminDNs adminDns, - final XFFResolver xffResolver, final AuditLog auditLog, final ThreadPool threadPool) { + public BackendRegistry( + final Settings settings, + final AdminDNs adminDns, + final XFFResolver xffResolver, + final AuditLog auditLog, + final ThreadPool threadPool + ) { this.adminDns = adminDns; this.opensearchSettings = settings; this.xffResolver = xffResolver; @@ -130,11 +140,10 @@ public BackendRegistry(final Settings settings, final AdminDNs adminDns, this.threadPool = threadPool; this.userInjector = new UserInjector(settings, threadPool, auditLog, xffResolver); - this.ttlInMin = settings.getAsInt(ConfigConstants.SECURITY_CACHE_TTL_MINUTES, 60); // This is going to be defined in the opensearch.yml, so it's best suited to be initialized once. - this.injectedUserEnabled = opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_USER_ENABLED,false); + this.injectedUserEnabled = opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_INJECT_USER_ENABLED, false); createCaches(); } @@ -153,8 +162,8 @@ public void invalidateCache() { public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { invalidateCache(); - anonymousAuthEnabled = dcm.isAnonymousAuthenticationEnabled()//config.dynamic.http.anonymous_auth_enabled - && !opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION, false); + anonymousAuthEnabled = dcm.isAnonymousAuthenticationEnabled()// config.dynamic.http.anonymous_auth_enabled + && !opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_COMPLIANCE_DISABLE_ANONYMOUS_AUTHENTICATION, false); restAuthDomains = Collections.unmodifiableSortedSet(dcm.getRestAuthDomains()); restAuthorizers = Collections.unmodifiableSet(dcm.getRestAuthorizers()); @@ -164,8 +173,8 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { ipClientBlockRegistries = dcm.getIpClientBlockRegistries(); authBackendClientBlockRegistries = dcm.getAuthBackendClientBlockRegistries(); - //OpenSearch Security no default authc - initialized = !restAuthDomains.isEmpty() || anonymousAuthEnabled || injectedUserEnabled; + // OpenSearch Security no default authc + initialized = !restAuthDomains.isEmpty() || anonymousAuthEnabled || injectedUserEnabled; } /** @@ -177,7 +186,8 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { */ public boolean authenticate(final RestRequest request, final RestChannel channel, final ThreadContext threadContext) { final boolean isDebugEnabled = log.isDebugEnabled(); - if (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress())) { + if (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress + && isBlocked(((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress())) { if (isDebugEnabled) { log.debug("Rejecting REST request because of blocked address: {}", request.getHttpChannel().getRemoteAddress()); } @@ -189,8 +199,8 @@ public boolean authenticate(final RestRequest request, final RestChannel channel final String sslPrincipal = (String) threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL); - if(adminDns.isAdminDN(sslPrincipal)) { - //PKI authenticated REST call + 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; @@ -203,8 +213,7 @@ public boolean authenticate(final RestRequest request, final RestChannel channel if (!isInitialized()) { log.error("Not yet initialized (you may need to run securityadmin)"); - channel.sendResponse(new BytesRestResponse(RestStatus.SERVICE_UNAVAILABLE, - "OpenSearch Security not initialized.")); + channel.sendResponse(new BytesRestResponse(RestStatus.SERVICE_UNAVAILABLE, "OpenSearch Security not initialized.")); return false; } @@ -212,7 +221,7 @@ public boolean authenticate(final RestRequest request, final RestChannel channel final boolean isTraceEnabled = log.isTraceEnabled(); if (isTraceEnabled) { log.trace("Rest authentication request from {} [original: {}]", remoteAddress, request.getHttpChannel().getRemoteAddress()); - } + } threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, remoteAddress); @@ -224,17 +233,20 @@ public boolean authenticate(final RestRequest request, final RestChannel channel HTTPAuthenticator firstChallengingHttpAuthenticator = null; - //TODO: ADD OUR AUTHC BACKEND IN/BEFORE THIS LIST - - //loop over all http/rest auth domains - for (final AuthDomain authDomain: restAuthDomains) { + // loop over all http/rest auth domains + for (final AuthDomain authDomain : restAuthDomains) { if (isDebugEnabled) { - log.debug("Check authdomain for rest {}/{} or {} in total", authDomain.getBackend().getType(), authDomain.getOrder(), restAuthDomains.size()); + log.debug( + "Check authdomain for rest {}/{} or {} in total", + authDomain.getBackend().getType(), + authDomain.getOrder(), + restAuthDomains.size() + ); } final HTTPAuthenticator httpAuthenticator = authDomain.getHttpAuthenticator(); - if(authDomain.isChallenge() && firstChallengingHttpAuthenticator == null) { + if (authDomain.isChallenge() && firstChallengingHttpAuthenticator == null) { firstChallengingHttpAuthenticator = httpAuthenticator; } @@ -262,17 +274,17 @@ public boolean authenticate(final RestRequest request, final RestChannel channel authCredenetials = ac; if (ac == null) { - //no credentials found in request - if(anonymousAuthEnabled) { + // no credentials found in request + if (anonymousAuthEnabled) { continue; } - if(authDomain.isChallenge() && httpAuthenticator.reRequestAuthentication(channel, null)) { + if (authDomain.isChallenge() && httpAuthenticator.reRequestAuthentication(channel, null)) { auditLog.logFailedLogin("", false, null, request); log.warn("No 'Authorization' header, send 401 and 'WWW-Authenticate Basic'"); return false; } else { - //no reRequest possible + // no reRequest possible if (isTraceEnabled) { log.trace("No 'Authorization' header, send 403"); } @@ -281,39 +293,54 @@ public boolean authenticate(final RestRequest request, final RestChannel channel } 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 + // 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 + // no reRequest possible continue; } } } - //http completed + // http completed authenticatedUser = authcz(userCache, restRoleCache, ac, authDomain.getBackend(), restAuthorizers); - if(authenticatedUser == null) { + if (authenticatedUser == null) { if (isDebugEnabled) { - log.debug("Cannot authenticate rest user {} (or add roles) with authdomain {}/{} of {}, try next", ac.getUsername(), authDomain.getBackend().getType(), authDomain.getOrder(), restAuthDomains); + log.debug( + "Cannot authenticate rest user {} (or add roles) with authdomain {}/{} of {}, try next", + ac.getUsername(), + authDomain.getBackend().getType(), + authDomain.getOrder(), + restAuthDomains + ); } - for (AuthFailureListener authFailureListener : this.authBackendFailureListeners.get(authDomain.getBackend().getClass().getName())) { + for (AuthFailureListener authFailureListener : this.authBackendFailureListeners.get( + authDomain.getBackend().getClass().getName() + )) { authFailureListener.onAuthFailure( - (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() - : null, - ac, request); + (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) + ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() + : null, + ac, + request + ); } continue; } - if(adminDns.isAdmin(authenticatedUser)) { + if (adminDns.isAdmin(authenticatedUser)) { log.error("Cannot authenticate rest 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")); + channel.sendResponse( + new BytesRestResponse( + RestStatus.FORBIDDEN, + "Cannot authenticate user because admin user is not permitted to login via HTTP" + ) + ); return false; } @@ -327,19 +354,26 @@ public boolean authenticate(final RestRequest request, final RestChannel channel authenticatedUser.setRequestedTenant(tenant); authenticated = true; break; - }//end looping auth domains + }// end looping auth domains - if(authenticated) { + 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); + threadContext.putTransient( + ConfigConstants.OPENDISTRO_SECURITY_USER, + impersonatedUser == null ? authenticatedUser : impersonatedUser + ); + auditLog.logSucceededLogin( + (impersonatedUser == null ? authenticatedUser : impersonatedUser).getName(), + false, + authenticatedUser.getName(), + request + ); } else { if (isDebugEnabled) { log.debug("User still not authenticated after checking {} auth domains", restAuthDomains.size()); } - if(authCredenetials == null && anonymousAuthEnabled) { + if (authCredenetials == null && anonymousAuthEnabled) { final String tenant = Utils.coalesce(request.header("securitytenant"), request.header("security_tenant")); User anonymousUser = new User(User.ANONYMOUS.getName(), new HashSet(User.ANONYMOUS.getRoles()), null); anonymousUser.setRequestedTenant(tenant); @@ -352,26 +386,33 @@ public boolean authenticate(final RestRequest request, final RestChannel channel return true; } - if(firstChallengingHttpAuthenticator != null) { + if (firstChallengingHttpAuthenticator != null) { if (isDebugEnabled) { log.debug("Rerequest with {}", firstChallengingHttpAuthenticator.getClass()); } - if(firstChallengingHttpAuthenticator.reRequestAuthentication(channel, null)) { + if (firstChallengingHttpAuthenticator.reRequestAuthentication(channel, null)) { if (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); + 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); + log.warn( + "Authentication finally failed for {} from {}", + authCredenetials == null ? null : authCredenetials.getUsername(), + remoteAddress + ); + auditLog.logFailedLogin(authCredenetials == null ? null : authCredenetials.getUsername(), false, null, request); notifyIpAuthFailureListeners(request, authCredenetials); @@ -384,8 +425,12 @@ public boolean authenticate(final RestRequest request, final RestChannel channel private void notifyIpAuthFailureListeners(RestRequest request, AuthCredentials authCredentials) { notifyIpAuthFailureListeners( - (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() : null, - authCredentials, request); + (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) + ? ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() + : null, + authCredentials, + request + ); } private void notifyIpAuthFailureListeners(InetAddress remoteAddress, AuthCredentials authCredentials, Object request) { @@ -399,9 +444,13 @@ private void notifyIpAuthFailureListeners(InetAddress remoteAddress, AuthCredent * * @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) { + private User checkExistsAndAuthz( + final Cache cache, + final User user, + final AuthenticationBackend authenticationBackend, + final Set authorizers + ) { + if (user == null) { return null; } @@ -409,14 +458,18 @@ private User checkExistsAndAuthz(final Cache cache, final User use final boolean isTraceEnabled = log.isTraceEnabled(); try { - return cache.get(user.getName(), new Callable() { //no cache miss in case of noop + return cache.get(user.getName(), new Callable() { // no cache miss in case of noop @Override public User call() throws Exception { if (isTraceEnabled) { - log.trace("Credentials for user {} not cached, return from {} backend directly", user.getName(), authenticationBackend.getType()); + log.trace( + "Credentials for user {} not cached, return from {} backend directly", + user.getName(), + authenticationBackend.getType() + ); } - if(authenticationBackend.exists(user)) { - authz(user, null, authorizers); //no role cache because no miss here in case of noop + if (authenticationBackend.exists(user)) { + authz(user, null, authorizers); // no role cache because no miss here in case of noop return user; } @@ -433,23 +486,24 @@ public User call() throws Exception { return null; } } + private void authz(User authenticatedUser, Cache> roleCache, final Set authorizers) { - if(authenticatedUser == null) { + if (authenticatedUser == null) { return; } - if(roleCache != null) { + if (roleCache != null) { final Set cachedBackendRoles = roleCache.getIfPresent(authenticatedUser); - if(cachedBackendRoles != null) { + if (cachedBackendRoles != null) { authenticatedUser.addRoles(new HashSet(cachedBackendRoles)); return; } } - if(authorizers == null || authorizers.isEmpty()) { + if (authorizers == null || authorizers.isEmpty()) { return; } @@ -457,7 +511,11 @@ private void authz(User authenticatedUser, Cache> roleCache, f for (final AuthorizationBackend ab : authorizers) { try { if (isTraceEnabled) { - log.trace("Backend roles for {} not cached, return from {} backend directly", authenticatedUser.getName(), ab.getType()); + log.trace( + "Backend roles for {} not cached, return from {} backend directly", + authenticatedUser.getName(), + ab.getType() + ); } ab.fillRoles(authenticatedUser, new AuthCredentials(authenticatedUser.getName())); } catch (Exception e) { @@ -465,7 +523,7 @@ private void authz(User authenticatedUser, Cache> roleCache, f } } - if(roleCache != null) { + if (roleCache != null) { roleCache.put(authenticatedUser, new HashSet(authenticatedUser.getRoles())); } } @@ -475,17 +533,22 @@ private void authz(User authenticatedUser, Cache> roleCache, f * * @return null if user cannot b authenticated */ - private User authcz(final Cache cache, Cache> roleCache, final AuthCredentials ac, - final AuthenticationBackend authBackend, final Set authorizers) { - if(ac == null) { + private User authcz( + final Cache cache, + Cache> roleCache, + 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 + // 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); } @@ -493,7 +556,11 @@ private User authcz(final Cache cache, Cache injectUserAndRoles(TransportRequest transportRequest, String String[] strs = injectedUserAndRoles.split("\\|"); if (strs.length == 0) { - log.error("Roles injected string malformed, could not extract parts. User string was '{}.'" + - " Roles injection failed.", injectedUserAndRoles); + log.error( + "Roles injected string malformed, could not extract parts. User string was '{}.'" + " Roles injection failed.", + injectedUserAndRoles + ); return null; } @@ -71,16 +73,20 @@ public Set injectUserAndRoles(TransportRequest transportRequest, String } Set roles = ImmutableSet.copyOf(strs[1].split(",")); - if(user != null && roles != null) { + if (user != null && roles != null) { addUser(user, transportRequest, action, task, ctx); } return roles; } - private void addUser(final User user, final TransportRequest transportRequest, - final String action, final Task task, final ThreadContext threadContext) { - if(threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER) != null) - return; + private void addUser( + final User user, + final TransportRequest transportRequest, + final String action, + final Task task, + final ThreadContext threadContext + ) { + if (threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER) != null) return; threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); } diff --git a/src/main/java/org/opensearch/security/auth/UserInjector.java b/src/main/java/org/opensearch/security/auth/UserInjector.java index 9253023eb3..9ce040c485 100644 --- a/src/main/java/org/opensearch/security/auth/UserInjector.java +++ b/src/main/java/org/opensearch/security/auth/UserInjector.java @@ -172,16 +172,16 @@ public InjectedUser getInjectedUser() { return injectedUser; } - boolean injectUser(RestRequest request) { InjectedUser injectedUser = getInjectedUser(); - if(injectedUser == null) { + if (injectedUser == null) { return false; } // Set remote address into the thread context if (injectedUser.getTransportAddress() != null) { - threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, injectedUser.getTransportAddress()); + threadPool.getThreadContext() + .putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, injectedUser.getTransportAddress()); } else { threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, xffResolver.resolve(request)); } diff --git a/src/main/java/org/opensearch/security/auth/blocking/ClientBlockRegistry.java b/src/main/java/org/opensearch/security/auth/blocking/ClientBlockRegistry.java index e74c3ad70a..801dafc75d 100644 --- a/src/main/java/org/opensearch/security/auth/blocking/ClientBlockRegistry.java +++ b/src/main/java/org/opensearch/security/auth/blocking/ClientBlockRegistry.java @@ -20,6 +20,8 @@ public interface ClientBlockRegistry { boolean isBlocked(ClientIdType clientId); + void block(ClientIdType clientId); + Class getClientIdType(); } diff --git a/src/main/java/org/opensearch/security/auth/blocking/HeapBasedClientBlockRegistry.java b/src/main/java/org/opensearch/security/auth/blocking/HeapBasedClientBlockRegistry.java index 450dda54db..334073118a 100644 --- a/src/main/java/org/opensearch/security/auth/blocking/HeapBasedClientBlockRegistry.java +++ b/src/main/java/org/opensearch/security/auth/blocking/HeapBasedClientBlockRegistry.java @@ -35,15 +35,19 @@ public class HeapBasedClientBlockRegistry implements ClientBlockRe public HeapBasedClientBlockRegistry(long expiryMs, int maxEntries, Class clientIdType) { this.clientIdType = clientIdType; - this.cache = CacheBuilder.newBuilder().expireAfterWrite(expiryMs, TimeUnit.MILLISECONDS).maximumSize(maxEntries).concurrencyLevel(4) - .removalListener(new RemovalListener() { - @Override - public void onRemoval(RemovalNotification notification) { - if (log.isInfoEnabled()) { - log.info("Unblocking " + notification.getKey()); - } + this.cache = CacheBuilder.newBuilder() + .expireAfterWrite(expiryMs, TimeUnit.MILLISECONDS) + .maximumSize(maxEntries) + .concurrencyLevel(4) + .removalListener(new RemovalListener() { + @Override + public void onRemoval(RemovalNotification notification) { + if (log.isInfoEnabled()) { + log.info("Unblocking " + notification.getKey()); } - }).build(); + } + }) + .build(); } @Override diff --git a/src/main/java/org/opensearch/security/auth/internal/InternalAuthenticationBackend.java b/src/main/java/org/opensearch/security/auth/internal/InternalAuthenticationBackend.java index adc49c8735..98443a2902 100644 --- a/src/main/java/org/opensearch/security/auth/internal/InternalAuthenticationBackend.java +++ b/src/main/java/org/opensearch/security/auth/internal/InternalAuthenticationBackend.java @@ -52,27 +52,27 @@ public class InternalAuthenticationBackend implements AuthenticationBackend, Aut @Override public boolean exists(User user) { - if(user == null || internalUsersModel == null) { + if (user == null || internalUsersModel == null) { return false; } final boolean exists = internalUsersModel.exists(user.getName()); - if(exists) { + if (exists) { user.addRoles(internalUsersModel.getBackenRoles(user.getName())); - //FIX https://github.com/opendistro-for-elasticsearch/security/pull/23 - //Credits to @turettn + // FIX https://github.com/opendistro-for-elasticsearch/security/pull/23 + // Credits to @turettn final Map customAttributes = internalUsersModel.getAttributes(user.getName()); Map attributeMap = new HashMap<>(); - if(customAttributes != null) { - for(Entry attributeEntry: customAttributes.entrySet()) { - attributeMap.put("attr.internal."+attributeEntry.getKey(), attributeEntry.getValue()); + if (customAttributes != null) { + for (Entry attributeEntry : customAttributes.entrySet()) { + attributeMap.put("attr.internal." + attributeEntry.getKey(), attributeEntry.getValue()); } } final List securityRoles = internalUsersModel.getSecurityRoles(user.getName()); - if(securityRoles != null) { + if (securityRoles != null) { user.addSecurityRoles(securityRoles); } @@ -104,10 +104,11 @@ public User authenticate(final AuthCredentials credentials) { final byte[] password; String hash; - if(!internalUsersModel.exists(credentials.getUsername())) { + if (!internalUsersModel.exists(credentials.getUsername())) { userExists = false; password = credentials.getPassword(); - hash = "$2y$12$NmKhjNssNgSIj8iXT7SYxeXvMA1E95a9tCt4cySY9FrQ4fB18xEc2"; // Ensure the same cryptographic complexity for users not found and invalid password + hash = "$2y$12$NmKhjNssNgSIj8iXT7SYxeXvMA1E95a9tCt4cySY9FrQ4fB18xEc2"; // Ensure the same cryptographic complexity for users not + // found and invalid password } else { userExists = true; password = credentials.getPassword(); @@ -123,22 +124,22 @@ public User authenticate(final AuthCredentials credentials) { char[] array = new char[buf.limit()]; buf.get(array); - Arrays.fill(password, (byte)0); + Arrays.fill(password, (byte) 0); try { if (passwordMatchesHash(hash, array) && userExists) { final List roles = internalUsersModel.getBackenRoles(credentials.getUsername()); final Map customAttributes = internalUsersModel.getAttributes(credentials.getUsername()); - if(customAttributes != null) { - for(Entry attributeName: customAttributes.entrySet()) { - credentials.addAttribute("attr.internal."+attributeName.getKey(), attributeName.getValue()); + if (customAttributes != null) { + for (Entry attributeName : customAttributes.entrySet()) { + credentials.addAttribute("attr.internal." + attributeName.getKey(), attributeName.getValue()); } } final User user = new User(credentials.getUsername(), roles, credentials); final List securityRoles = internalUsersModel.getSecurityRoles(credentials.getUsername()); - if(securityRoles != null) { + if (securityRoles != null) { user.addSecurityRoles(securityRoles); } return user; @@ -149,7 +150,7 @@ public User authenticate(final AuthCredentials credentials) { throw new OpenSearchSecurityException("password does not match"); } } finally { - Arrays.fill(wrap.array(), (byte)0); + Arrays.fill(wrap.array(), (byte) 0); Arrays.fill(buf.array(), '\0'); Arrays.fill(array, '\0'); } @@ -164,18 +165,19 @@ public String getType() { public void fillRoles(User user, AuthCredentials credentials) throws OpenSearchSecurityException { if (internalUsersModel == null) { - throw new OpenSearchSecurityException("Internal authentication backend not configured. May be OpenSearch Security is not initialized."); + throw new OpenSearchSecurityException( + "Internal authentication backend not configured. May be OpenSearch Security is not initialized." + ); } - if(exists(user)) { + if (exists(user)) { final List roles = internalUsersModel.getBackenRoles(user.getName()); - if(roles != null && !roles.isEmpty() && user != null) { + if (roles != null && !roles.isEmpty() && user != null) { user.addRoles(roles); } } - } @Subscribe @@ -183,5 +185,4 @@ public void onInternalUsersModelChanged(InternalUsersModel ium) { this.internalUsersModel = ium; } - } diff --git a/src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java b/src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java index a4d596b61d..0fc796d94f 100644 --- a/src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java +++ b/src/main/java/org/opensearch/security/auth/limiting/AbstractRateLimiter.java @@ -32,10 +32,16 @@ public abstract class AbstractRateLimiter implements AuthFailureLi protected final RateTracker rateTracker; public AbstractRateLimiter(Settings settings, Path configPath, Class clientIdType) { - this.clientBlockRegistry = new HeapBasedClientBlockRegistry<>(settings.getAsInt("block_expiry_seconds", 60 * 10) * 1000, - settings.getAsInt("max_blocked_clients", 100_000), clientIdType); - this.rateTracker = RateTracker.create(settings.getAsInt("time_window_seconds", 60 * 60) * 1000, settings.getAsInt("allowed_tries", 10), - settings.getAsInt("max_tracked_clients", 100_000)); + this.clientBlockRegistry = new HeapBasedClientBlockRegistry<>( + settings.getAsInt("block_expiry_seconds", 60 * 10) * 1000, + settings.getAsInt("max_blocked_clients", 100_000), + clientIdType + ); + this.rateTracker = RateTracker.create( + settings.getAsInt("time_window_seconds", 60 * 60) * 1000, + settings.getAsInt("allowed_tries", 10), + settings.getAsInt("max_tracked_clients", 100_000) + ); } @Override diff --git a/src/main/java/org/opensearch/security/auth/limiting/AddressBasedRateLimiter.java b/src/main/java/org/opensearch/security/auth/limiting/AddressBasedRateLimiter.java index 35a6571f8f..876615870a 100644 --- a/src/main/java/org/opensearch/security/auth/limiting/AddressBasedRateLimiter.java +++ b/src/main/java/org/opensearch/security/auth/limiting/AddressBasedRateLimiter.java @@ -25,7 +25,10 @@ import org.opensearch.security.auth.blocking.ClientBlockRegistry; import org.opensearch.security.user.AuthCredentials; -public class AddressBasedRateLimiter extends AbstractRateLimiter implements AuthFailureListener, ClientBlockRegistry { +public class AddressBasedRateLimiter extends AbstractRateLimiter + implements + AuthFailureListener, + ClientBlockRegistry { public AddressBasedRateLimiter(Settings settings, Path configPath) { super(settings, configPath, InetAddress.class); diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java index 16d1248820..54461f6b7e 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/EncryptionDecryptionUtil.java @@ -33,8 +33,7 @@ public static String encrypt(final String secret, final String data) { byte[] cipherText = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(cipherText); } catch (Exception e) { - throw new RuntimeException( - "Error occured while encrypting data", e); + throw new RuntimeException("Error occured while encrypting data", e); } } diff --git a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java index 5328453c98..f2f59b111b 100644 --- a/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java +++ b/src/main/java/org/opensearch/security/authtoken/jwt/JwtVendor.java @@ -49,7 +49,7 @@ public class JwtVendor { private JoseJwtProducer jwtProducer; private final LongSupplier timeProvider; - //TODO: Relocate/Remove them at once we make the descisions about the `roles` + // TODO: Relocate/Remove them at once we make the descisions about the `roles` private ConfigModel configModel; private ThreadContext threadContext; @@ -69,7 +69,7 @@ public JwtVendor(Settings settings) { timeProvider = System::currentTimeMillis; } - //For testing the expiration in the future + // For testing the expiration in the future public JwtVendor(Settings settings, final LongSupplier timeProvider) { JoseJwtProducer jwtProducer = new JoseJwtProducer(); try { @@ -109,8 +109,7 @@ static JsonWebKey createJwkFromSettings(Settings settings) throws Exception { Settings jwkSettings = settings.getAsSettings("jwt").getAsSettings("key"); if (jwkSettings.isEmpty()) { - throw new Exception( - "Settings for key is missing. Please specify at least the option signing_key with a shared secret."); + throw new Exception("Settings for key is missing. Please specify at least the option signing_key with a shared secret."); } JsonWebKey jwk = new JsonWebKey(); @@ -123,7 +122,7 @@ static JsonWebKey createJwkFromSettings(Settings settings) throws Exception { } } - //TODO:Getting roles from User + // TODO:Getting roles from User public Map prepareClaimsForUser(User user, ThreadPool threadPool) { Map claims = new HashMap<>(); this.threadContext = threadPool.getThreadContext(); @@ -166,7 +165,7 @@ public String createJwt(String issuer, String subject, String audience, Integer throw new Exception("The expiration time should be a positive integer"); } - //TODO: IF USER ENABLES THE BWC MODE, WE ARE EXPECTING TO SET PLAIN TEXT ROLE AS `dr` + // TODO: IF USER ENABLES THE BWC MODE, WE ARE EXPECTING TO SET PLAIN TEXT ROLE AS `dr` if (roles != null) { String listOfRoles = String.join(",", roles); jwtClaims.setProperty("er", EncryptionDecryptionUtil.encrypt(claimsEncryptionKey, listOfRoles)); @@ -178,12 +177,12 @@ public String createJwt(String issuer, String subject, String audience, Integer if (logger.isDebugEnabled()) { logger.debug( - "Created JWT: " - + encodedJwt - + "\n" - + jsonMapReaderWriter.toJson(jwt.getJwsHeaders()) - + "\n" - + JwtUtils.claimsToJson(jwt.getClaims()) + "Created JWT: " + + encodedJwt + + "\n" + + jsonMapReaderWriter.toJson(jwt.getJwsHeaders()) + + "\n" + + JwtUtils.claimsToJson(jwt.getClaims()) ); } diff --git a/src/main/java/org/opensearch/security/compliance/ComplianceConfig.java b/src/main/java/org/opensearch/security/compliance/ComplianceConfig.java index 4b0f3079f2..1d81479f37 100644 --- a/src/main/java/org/opensearch/security/compliance/ComplianceConfig.java +++ b/src/main/java/org/opensearch/security/compliance/ComplianceConfig.java @@ -105,19 +105,20 @@ public class ComplianceConfig { private final boolean enabled; private ComplianceConfig( - final boolean enabled, - final boolean logExternalConfig, - final boolean logInternalConfig, - final boolean logReadMetadataOnly, - final Map> watchedReadFields, - final Set ignoredComplianceUsersForRead, - final boolean logWriteMetadataOnly, - final boolean logDiffsForWrite, - final List watchedWriteIndicesPatterns, - final Set ignoredComplianceUsersForWrite, - final String securityIndex, - final String destinationType, - final String destinationIndex) { + final boolean enabled, + final boolean logExternalConfig, + final boolean logInternalConfig, + final boolean logReadMetadataOnly, + final Map> watchedReadFields, + final Set ignoredComplianceUsersForRead, + final boolean logWriteMetadataOnly, + final boolean logDiffsForWrite, + final List watchedWriteIndicesPatterns, + final Set ignoredComplianceUsersForWrite, + final String securityIndex, + final String destinationType, + final String destinationIndex + ) { this.enabled = enabled; this.logExternalConfig = logExternalConfig; this.logInternalConfig = logInternalConfig; @@ -133,22 +134,20 @@ private ComplianceConfig( this.watchedWriteIndicesPatterns = watchedWriteIndicesPatterns; this.ignoredComplianceUsersForWrite = ignoredComplianceUsersForWrite; - this.readEnabledFields = watchedReadFields.entrySet().stream() - .filter(entry -> !Strings.isNullOrEmpty(entry.getKey())) - .collect( - ImmutableMap.toImmutableMap( - entry -> WildcardMatcher.from(entry.getKey()), - entry -> ImmutableSet.copyOf(entry.getValue()) - ) - ); + this.readEnabledFields = watchedReadFields.entrySet() + .stream() + .filter(entry -> !Strings.isNullOrEmpty(entry.getKey())) + .collect( + ImmutableMap.toImmutableMap(entry -> WildcardMatcher.from(entry.getKey()), entry -> ImmutableSet.copyOf(entry.getValue())) + ); DateTimeFormatter auditLogPattern = null; String auditLogIndex = null; if (INTERNAL_OPENSEARCH.equalsIgnoreCase(destinationType)) { try { - auditLogPattern = DateTimeFormat.forPattern(destinationIndex); //throws IllegalArgumentException if no pattern + auditLogPattern = DateTimeFormat.forPattern(destinationIndex); // throws IllegalArgumentException if no pattern } catch (IllegalArgumentException e) { - //no pattern + // no pattern auditLogIndex = destinationIndex; } catch (Exception e) { log.error("Unable to check if auditlog index {} is part of compliance setup", destinationIndex, e); @@ -157,43 +156,45 @@ private ComplianceConfig( this.auditLogPattern = auditLogPattern; this.auditLogIndex = auditLogIndex; - this.readEnabledFieldsCache = CacheBuilder.newBuilder() - .maximumSize(CACHE_SIZE) - .build(new CacheLoader() { - @Override - public WildcardMatcher load(String index) throws Exception { - return WildcardMatcher.from(getFieldsForIndex(index)); - } - }); + this.readEnabledFieldsCache = CacheBuilder.newBuilder().maximumSize(CACHE_SIZE).build(new CacheLoader() { + @Override + public WildcardMatcher load(String index) throws Exception { + return WildcardMatcher.from(getFieldsForIndex(index)); + } + }); } @VisibleForTesting public ComplianceConfig( - final boolean enabled, - final boolean logExternalConfig, - final boolean logInternalConfig, - final boolean logReadMetadataOnly, - final Map> watchedReadFields, - final Set ignoredComplianceUsersForRead, - final boolean logWriteMetadataOnly, - final boolean logDiffsForWrite, - final List watchedWriteIndicesPatterns, - final Set ignoredComplianceUsersForWrite, - Settings settings) { + final boolean enabled, + final boolean logExternalConfig, + final boolean logInternalConfig, + final boolean logReadMetadataOnly, + final Map> watchedReadFields, + final Set ignoredComplianceUsersForRead, + final boolean logWriteMetadataOnly, + final boolean logDiffsForWrite, + final List watchedWriteIndicesPatterns, + final Set ignoredComplianceUsersForWrite, + Settings settings + ) { this( - enabled, - logExternalConfig, - logInternalConfig, - logReadMetadataOnly, - watchedReadFields, - ignoredComplianceUsersForRead, - logWriteMetadataOnly, - logDiffsForWrite, - watchedWriteIndicesPatterns, - ignoredComplianceUsersForWrite, - settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX), - settings.get(ConfigConstants.SECURITY_AUDIT_TYPE_DEFAULT, null), - settings.get(ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_OPENSEARCH_INDEX, "'security-auditlog-'YYYY.MM.dd") + enabled, + logExternalConfig, + logInternalConfig, + logReadMetadataOnly, + watchedReadFields, + ignoredComplianceUsersForRead, + logWriteMetadataOnly, + logDiffsForWrite, + watchedWriteIndicesPatterns, + ignoredComplianceUsersForWrite, + settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX), + settings.get(ConfigConstants.SECURITY_AUDIT_TYPE_DEFAULT, null), + settings.get( + ConfigConstants.SECURITY_AUDIT_CONFIG_DEFAULT_PREFIX + ConfigConstants.SECURITY_AUDIT_OPENSEARCH_INDEX, + "'security-auditlog-'YYYY.MM.dd" + ) ); } @@ -215,7 +216,14 @@ public void log(Logger logger) { @JsonCreator public static ComplianceConfig from(Map properties, @JacksonInject Settings settings) throws JsonProcessingException { if (!FIELDS.containsAll(properties.keySet())) { - throw new UnrecognizedPropertyException(null, "Invalid property present in the input data for compliance config", null, ComplianceConfig.class, null, null); + throw new UnrecognizedPropertyException( + null, + "Invalid property present in the input data for compliance config", + null, + ComplianceConfig.class, + null, + null + ); } final boolean enabled = getOrDefault(properties, "enabled", true); @@ -223,24 +231,28 @@ public static ComplianceConfig from(Map properties, @JacksonInje final boolean logInternalConfig = getOrDefault(properties, "internal_config", false); final boolean logReadMetadataOnly = getOrDefault(properties, "read_metadata_only", false); final Map> watchedReadFields = getOrDefault(properties, "read_watched_fields", Collections.emptyMap()); - final Set ignoredComplianceUsersForRead = ImmutableSet.copyOf(getOrDefault(properties, "read_ignore_users", AuditConfig.DEFAULT_IGNORED_USERS)); + final Set ignoredComplianceUsersForRead = ImmutableSet.copyOf( + getOrDefault(properties, "read_ignore_users", AuditConfig.DEFAULT_IGNORED_USERS) + ); final boolean logWriteMetadataOnly = getOrDefault(properties, "write_metadata_only", false); final boolean logDiffsForWrite = getOrDefault(properties, "write_log_diffs", false); final List watchedWriteIndicesPatterns = getOrDefault(properties, "write_watched_indices", Collections.emptyList()); - final Set ignoredComplianceUsersForWrite = ImmutableSet.copyOf(getOrDefault(properties, "write_ignore_users", AuditConfig.DEFAULT_IGNORED_USERS)); + final Set ignoredComplianceUsersForWrite = ImmutableSet.copyOf( + getOrDefault(properties, "write_ignore_users", AuditConfig.DEFAULT_IGNORED_USERS) + ); return new ComplianceConfig( - enabled, - logExternalConfig, - logInternalConfig, - logReadMetadataOnly, - watchedReadFields, - ignoredComplianceUsersForRead, - logWriteMetadataOnly, - logDiffsForWrite, - watchedWriteIndicesPatterns, - ignoredComplianceUsersForWrite, - settings + enabled, + logExternalConfig, + logInternalConfig, + logReadMetadataOnly, + watchedReadFields, + ignoredComplianceUsersForRead, + logWriteMetadataOnly, + logDiffsForWrite, + watchedWriteIndicesPatterns, + ignoredComplianceUsersForWrite, + settings ); } @@ -250,47 +262,71 @@ public static ComplianceConfig from(Map properties, @JacksonInje * @return compliance configuration */ public static ComplianceConfig from(Settings settings) { - final boolean logExternalConfig = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED, false); + final boolean logExternalConfig = settings.getAsBoolean( + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_EXTERNAL_CONFIG_ENABLED, + false + ); final boolean logInternalConfig = settings.getAsBoolean(ConfigConstants.SECURITY_COMPLIANCE_HISTORY_INTERNAL_CONFIG_ENABLED, false); - final boolean logReadMetadataOnly = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_METADATA_ONLY, false); - final boolean logWriteMetadataOnly = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_METADATA_ONLY, false); - final boolean logDiffsForWrite = settings.getAsBoolean(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_LOG_DIFFS, false); - final List watchedReadFields = settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS, - Collections.emptyList(), false); - //plugins.security.compliance.pii_fields: - // - indexpattern,fieldpattern,fieldpattern,.... + final boolean logReadMetadataOnly = settings.getAsBoolean( + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_METADATA_ONLY, + false + ); + final boolean logWriteMetadataOnly = settings.getAsBoolean( + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_METADATA_ONLY, + false + ); + final boolean logDiffsForWrite = settings.getAsBoolean( + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_LOG_DIFFS, + false + ); + final List watchedReadFields = settings.getAsList( + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_WATCHED_FIELDS, + Collections.emptyList(), + false + ); + // plugins.security.compliance.pii_fields: + // - indexpattern,fieldpattern,fieldpattern,.... final Map> readEnabledFields = watchedReadFields.stream() - .map(watchedReadField -> watchedReadField.split(",")) - .filter(split -> split.length != 0 && !Strings.isNullOrEmpty(split[0])) - .collect(ImmutableMap.toImmutableMap( - split -> split[0], - split -> split.length == 1 ? - ImmutableList.of("*") : Arrays.stream(split).skip(1).collect(ImmutableList.toImmutableList()) - )); - final List watchedWriteIndices = settings.getAsList(ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES, Collections.emptyList()); + .map(watchedReadField -> watchedReadField.split(",")) + .filter(split -> split.length != 0 && !Strings.isNullOrEmpty(split[0])) + .collect( + ImmutableMap.toImmutableMap( + split -> split[0], + split -> split.length == 1 + ? ImmutableList.of("*") + : Arrays.stream(split).skip(1).collect(ImmutableList.toImmutableList()) + ) + ); + final List watchedWriteIndices = settings.getAsList( + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_WATCHED_INDICES, + Collections.emptyList() + ); final Set ignoredComplianceUsersForRead = ConfigConstants.getSettingAsSet( - settings, - ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_IGNORE_USERS, - AuditConfig.DEFAULT_IGNORED_USERS, - false); + settings, + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_READ_IGNORE_USERS, + AuditConfig.DEFAULT_IGNORED_USERS, + false + ); final Set ignoredComplianceUsersForWrite = ConfigConstants.getSettingAsSet( - settings, - ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_IGNORE_USERS, - AuditConfig.DEFAULT_IGNORED_USERS, - false); + settings, + ConfigConstants.OPENDISTRO_SECURITY_COMPLIANCE_HISTORY_WRITE_IGNORE_USERS, + AuditConfig.DEFAULT_IGNORED_USERS, + false + ); return new ComplianceConfig( - true, - logExternalConfig, - logInternalConfig, - logReadMetadataOnly, - readEnabledFields, - ignoredComplianceUsersForRead, - logWriteMetadataOnly, - logDiffsForWrite, - watchedWriteIndices, - ignoredComplianceUsersForWrite, - settings); + true, + logExternalConfig, + logInternalConfig, + logReadMetadataOnly, + readEnabledFields, + ignoredComplianceUsersForRead, + logWriteMetadataOnly, + logDiffsForWrite, + watchedWriteIndices, + ignoredComplianceUsersForWrite, + settings + ); } /** @@ -415,10 +451,11 @@ private Set getFieldsForIndex(String index) { } } - return readEnabledFields.entrySet().stream() - .filter(entry -> entry.getKey().test(index)) - .flatMap(entry -> entry.getValue().stream()) - .collect(ImmutableSet.toImmutableSet()); + return readEnabledFields.entrySet() + .stream() + .filter(entry -> entry.getKey().test(index)) + .flatMap(entry -> entry.getValue().stream()) + .collect(ImmutableSet.toImmutableSet()); } /** diff --git a/src/main/java/org/opensearch/security/compliance/ComplianceIndexingOperationListener.java b/src/main/java/org/opensearch/security/compliance/ComplianceIndexingOperationListener.java index b7803a4836..3d31111407 100644 --- a/src/main/java/org/opensearch/security/compliance/ComplianceIndexingOperationListener.java +++ b/src/main/java/org/opensearch/security/compliance/ComplianceIndexingOperationListener.java @@ -37,6 +37,6 @@ public class ComplianceIndexingOperationListener implements IndexingOperationListener { public void setIs(IndexService is) { - //noop + // noop } } diff --git a/src/main/java/org/opensearch/security/compliance/ComplianceIndexingOperationListenerImpl.java b/src/main/java/org/opensearch/security/compliance/ComplianceIndexingOperationListenerImpl.java index c8834c9e31..cf369bb0cb 100644 --- a/src/main/java/org/opensearch/security/compliance/ComplianceIndexingOperationListenerImpl.java +++ b/src/main/java/org/opensearch/security/compliance/ComplianceIndexingOperationListenerImpl.java @@ -40,7 +40,7 @@ public ComplianceIndexingOperationListenerImpl(final AuditLog auditlog) { @Override public void setIs(final IndexService is) { - if(this.is != null) { + if (this.is != null) { throw new OpenSearchException("Index service already set"); } this.is = is; @@ -66,7 +66,9 @@ public void postDelete(final ShardId shardId, final Delete delete, final DeleteR final ComplianceConfig complianceConfig = auditlog.getComplianceConfig(); if (isLoggingWriteEnabled(complianceConfig, shardId.getIndexName())) { Objects.requireNonNull(is); - if(result.getFailure() == null && result.isFound() && delete.origin() == org.opensearch.index.engine.Engine.Operation.Origin.PRIMARY) { + if (result.getFailure() == null + && result.isFound() + && delete.origin() == org.opensearch.index.engine.Engine.Operation.Origin.PRIMARY) { auditlog.logDocumentDeleted(shardId, delete, result); } } @@ -83,15 +85,14 @@ public Index preIndex(final ShardId shardId, final Index index) { return index; } - if((shard = is.getShardOrNull(shardId.getId())) == null) { + if ((shard = is.getShardOrNull(shardId.getId())) == null) { return index; } if (shard.isReadAllowed()) { try { - final GetResult getResult = shard.getService().getForUpdate(index.id(), - index.getIfSeqNo(), index.getIfPrimaryTerm()); + final GetResult getResult = shard.getService().getForUpdate(index.id(), index.getIfSeqNo(), index.getIfPrimaryTerm()); if (getResult.isExists()) { threadContext.set(new Context(getResult)); @@ -113,7 +114,6 @@ public Index preIndex(final ShardId shardId, final Index index) { return index; } - @Override public void postIndex(final ShardId shardId, final Index index, final Exception ex) { if (isLoggingWriteDiffEnabled(auditlog.getComplianceConfig(), shardId.getIndexName())) { @@ -130,7 +130,7 @@ public void postIndex(ShardId shardId, Index index, IndexResult result) { if (complianceConfig.shouldLogDiffsForWrite()) { final Context context = threadContext.get(); - final GetResult previousContent = context==null?null:context.getGetResult(); + final GetResult previousContent = context == null ? null : context.getGetResult(); threadContext.remove(); Objects.requireNonNull(is); @@ -138,26 +138,31 @@ public void postIndex(ShardId shardId, Index index, IndexResult result) { return; } - if(is.getShardOrNull(shardId.getId()) == null) { + if (is.getShardOrNull(shardId.getId()) == null) { return; } - if(previousContent == null) { - //no previous content - if(!result.isCreated()) { - log.warn("No previous content and not created (its an update but do not find orig source) for {}/{}/{}", index.startTime(), shardId, index.id()); + if (previousContent == null) { + // no previous content + if (!result.isCreated()) { + log.warn( + "No previous content and not created (its an update but do not find orig source) for {}/{}/{}", + index.startTime(), + shardId, + index.id() + ); } - assert result.isCreated():"No previous content and not created"; + assert result.isCreated() : "No previous content and not created"; } else { - if(result.isCreated()) { + if (result.isCreated()) { log.warn("Previous content and created for {}/{}/{}", index.startTime(), shardId, index.id()); } - assert !result.isCreated():"Previous content and created"; + assert !result.isCreated() : "Previous content and created"; } auditlog.logDocumentWritten(shardId, previousContent, index, result); } else { - //no diffs + // no diffs if (result.getFailure() != null || index.origin() != org.opensearch.index.engine.Engine.Operation.Origin.PRIMARY) { return; } diff --git a/src/main/java/org/opensearch/security/compliance/FieldReadCallback.java b/src/main/java/org/opensearch/security/compliance/FieldReadCallback.java index ff507c2a73..73f536c2f8 100644 --- a/src/main/java/org/opensearch/security/compliance/FieldReadCallback.java +++ b/src/main/java/org/opensearch/security/compliance/FieldReadCallback.java @@ -46,8 +46,8 @@ public final class FieldReadCallback { private static final Logger log = LogManager.getLogger(FieldReadCallback.class); - //private final ThreadContext threadContext; - //private final ClusterService clusterService; + // private final ThreadContext threadContext; + // private final ClusterService clusterService; private final Index index; private final WildcardMatcher maskedFieldsMatcher; private final AuditLog auditLog; @@ -56,19 +56,24 @@ public final class FieldReadCallback { private Doc doc; private final ShardId shardId; - public FieldReadCallback(final ThreadContext threadContext, final IndexService indexService, - final ClusterService clusterService, final AuditLog auditLog, - final WildcardMatcher maskedFieldsMatcher, ShardId shardId) { + public FieldReadCallback( + final ThreadContext threadContext, + final IndexService indexService, + final ClusterService clusterService, + final AuditLog auditLog, + final WildcardMatcher maskedFieldsMatcher, + ShardId shardId + ) { super(); - //this.threadContext = Objects.requireNonNull(threadContext); - //this.clusterService = Objects.requireNonNull(clusterService); + // this.threadContext = Objects.requireNonNull(threadContext); + // this.clusterService = Objects.requireNonNull(clusterService); this.index = Objects.requireNonNull(indexService).index(); this.auditLog = auditLog; this.maskedFieldsMatcher = maskedFieldsMatcher; this.shardId = shardId; try { sfc = (SourceFieldsContext) HeaderHelper.deserializeSafeFromHeader(threadContext, "_opendistro_security_source_field_context"); - if(sfc != null && sfc.hasIncludesOrExcludes()) { + if (sfc != null && sfc.hasIncludesOrExcludes()) { if (log.isTraceEnabled()) { log.trace("_opendistro_security_source_field_context: {}", sfc); } @@ -76,39 +81,40 @@ public FieldReadCallback(final ThreadContext threadContext, final IndexService i filterFunction = XContentMapValues.filter(sfc.getIncludes(), sfc.getExcludes()); } } catch (Exception e) { - if(log.isDebugEnabled()) { + if (log.isDebugEnabled()) { log.debug("Cannot deserialize _opendistro_security_source_field_context because of {}", e.toString()); } } } private boolean recordField(final String fieldName, boolean isStringField) { - return !(isStringField && maskedFieldsMatcher.test(fieldName)) && auditLog.getComplianceConfig().readHistoryEnabledForField(index.getName(), fieldName); + return !(isStringField && maskedFieldsMatcher.test(fieldName)) + && auditLog.getComplianceConfig().readHistoryEnabledForField(index.getName(), fieldName); } public void binaryFieldRead(final FieldInfo fieldInfo, byte[] fieldValue) { try { - if(!recordField(fieldInfo.name, false) && !fieldInfo.name.equals("_source") && !fieldInfo.name.equals("_id")) { + if (!recordField(fieldInfo.name, false) && !fieldInfo.name.equals("_source") && !fieldInfo.name.equals("_id")) { return; } - if(fieldInfo.name.equals("_source")) { + if (fieldInfo.name.equals("_source")) { - if(filterFunction != null) { + if (filterFunction != null) { final Map filteredSource = filterFunction.apply(Utils.byteArrayToMutableJsonMap(fieldValue)); fieldValue = Utils.jsonMapToByteArray(filteredSource); } Map filteredSource = new JsonFlattener(new String(fieldValue, StandardCharsets.UTF_8)).flattenAsMap(); - for(String k: filteredSource.keySet()) { - if(!recordField(k, filteredSource.get(k) instanceof String)) { + for (String k : filteredSource.keySet()) { + if (!recordField(k, filteredSource.get(k) instanceof String)) { continue; } fieldRead0(k, filteredSource.get(k)); } } else if (fieldInfo.name.equals("_id")) { fieldRead0(fieldInfo.name, Uid.decodeId(fieldValue)); - } else { + } else { fieldRead0(fieldInfo.name, new String(fieldValue, StandardCharsets.UTF_8)); } } catch (Exception e) { @@ -118,7 +124,7 @@ public void binaryFieldRead(final FieldInfo fieldInfo, byte[] fieldValue) { public void stringFieldRead(final FieldInfo fieldInfo, final String fieldValue) { try { - if(!recordField(fieldInfo.name, true)) { + if (!recordField(fieldInfo.name, true)) { return; } fieldRead0(fieldInfo.name, fieldValue); @@ -129,7 +135,7 @@ public void stringFieldRead(final FieldInfo fieldInfo, final String fieldValue) public void numericFieldRead(final FieldInfo fieldInfo, final Number fieldValue) { try { - if(!recordField(fieldInfo.name, false)) { + if (!recordField(fieldInfo.name, false)) { return; } fieldRead0(fieldInfo.name, fieldValue); @@ -139,15 +145,15 @@ public void numericFieldRead(final FieldInfo fieldInfo, final Number fieldValue) } private void fieldRead0(final String fieldName, final Object fieldValue) { - if(doc != null) { - if(fieldName.equals("_id")) { + if (doc != null) { + if (fieldName.equals("_id")) { doc.setId(fieldValue.toString()); } else { doc.addField(new Field(fieldName, fieldValue)); } } else { final String indexName = index.getName(); - if(fieldName.equals("_id")) { + if (fieldName.equals("_id")) { doc = new Doc(indexName, fieldValue.toString()); } else { doc = new Doc(indexName, null); @@ -157,12 +163,12 @@ private void fieldRead0(final String fieldName, final Object fieldValue) { } public void finished() { - if(doc == null) { + if (doc == null) { return; } try { Map f = new HashMap(); - for(Field fi: doc.fields) { + for (Field fi : doc.fields) { f.put(fi.fieldName, String.valueOf(fi.fieldValue)); } auditLog.logDocumentRead(doc.indexName, doc.id, shardId, f); @@ -202,11 +208,13 @@ public String toString() { private class Field { final String fieldName; final Object fieldValue; + public Field(String fieldName, Object fieldValue) { super(); this.fieldName = fieldName; this.fieldValue = fieldValue; } + @Override public String toString() { return "Field [fieldName=" + fieldName + ", fieldValue=" + fieldValue + "]"; diff --git a/src/main/java/org/opensearch/security/configuration/AdminDNs.java b/src/main/java/org/opensearch/security/configuration/AdminDNs.java index 72a4485e9f..204f277808 100644 --- a/src/main/java/org/opensearch/security/configuration/AdminDNs.java +++ b/src/main/java/org/opensearch/security/configuration/AdminDNs.java @@ -63,7 +63,7 @@ public AdminDNs(final Settings settings) { final List adminDnsA = settings.getAsList(ConfigConstants.SECURITY_AUTHCZ_ADMIN_DN, Collections.emptyList()); - for (String dn:adminDnsA) { + for (String dn : adminDnsA) { try { log.debug("{} is registered as an admin dn", dn); adminDn.add(new LdapName(dn)); @@ -75,16 +75,17 @@ public AdminDNs(final Settings settings) { } adminUsernames.add(dn); } else { - log.error("Unable to parse admin dn {}",dn, e); + log.error("Unable to parse admin dn {}", dn, e); } } } - log.debug("Loaded {} admin DN's {}",adminDn.size(), adminDn); + log.debug("Loaded {} admin DN's {}", adminDn.size(), adminDn); - final Settings impersonationDns = settings.getByPrefix(ConfigConstants.SECURITY_AUTHCZ_IMPERSONATION_DN+"."); + final Settings impersonationDns = settings.getByPrefix(ConfigConstants.SECURITY_AUTHCZ_IMPERSONATION_DN + "."); - allowedDnsImpersonations = impersonationDns.keySet().stream() + allowedDnsImpersonations = impersonationDns.keySet() + .stream() .map(this::toLdapName) .filter(Objects::nonNull) .collect( @@ -96,17 +97,18 @@ public AdminDNs(final Settings settings) { log.debug("Loaded {} impersonation DN's {}", allowedDnsImpersonations.size(), allowedDnsImpersonations); - final Settings impersonationUsersRest = settings.getByPrefix(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+"."); + final Settings impersonationUsersRest = settings.getByPrefix(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + "."); - allowedRestImpersonations = impersonationUsersRest.keySet().stream() + allowedRestImpersonations = impersonationUsersRest.keySet() + .stream() .collect( ImmutableMap.toImmutableMap( Function.identity(), - user -> WildcardMatcher.from(settings.getAsList(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS+"."+user)) + user -> WildcardMatcher.from(settings.getAsList(ConfigConstants.SECURITY_AUTHCZ_REST_IMPERSONATION_USERS + "." + user)) ) ); - log.debug("Loaded {} impersonation users for REST {}",allowedRestImpersonations.size(), allowedRestImpersonations); + log.debug("Loaded {} impersonation users for REST {}", allowedRestImpersonations.size(), allowedRestImpersonations); } private LdapName toLdapName(String dn) { @@ -132,17 +134,17 @@ public boolean isAdmin(User user) { public boolean isAdminDN(String dn) { - if(dn == null) return false; + if (dn == null) return false; try { return isAdminDN(new LdapName(dn)); } catch (InvalidNameException e) { - return false; + return false; } } private boolean isAdminDN(LdapName dn) { - if(dn == null) return false; + if (dn == null) return false; boolean isAdmin = adminDn.contains(dn); @@ -154,6 +156,8 @@ private boolean isAdminDN(LdapName dn) { } public boolean isRestImpersonationAllowed(final String originalUser, final String impersonated) { - return (originalUser != null) ? allowedRestImpersonations.getOrDefault(originalUser, WildcardMatcher.NONE).test(impersonated) : false; + return (originalUser != null) + ? allowedRestImpersonations.getOrDefault(originalUser, WildcardMatcher.NONE).test(impersonated) + : false; } } diff --git a/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java b/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java index 1c42321986..00101d9a73 100644 --- a/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java +++ b/src/main/java/org/opensearch/security/configuration/ClusterInfoHolder.java @@ -43,7 +43,7 @@ public class ClusterInfoHolder implements ClusterStateListener { @Override public void clusterChanged(ClusterChangedEvent event) { - if(nodes == null || event.nodesChanged()) { + if (nodes == null || event.nodesChanged()) { nodes = event.state().nodes(); if (log.isDebugEnabled()) { log.debug("Cluster Info Holder now initialized for 'nodes'"); @@ -51,7 +51,7 @@ public void clusterChanged(ClusterChangedEvent event) { initialized = true; } - isLocalNodeElectedClusterManager = event.localNodeClusterManager()?Boolean.TRUE:Boolean.FALSE; + isLocalNodeElectedClusterManager = event.localNodeClusterManager() ? Boolean.TRUE : Boolean.FALSE; } public Boolean isLocalNodeElectedClusterManager() { @@ -63,13 +63,13 @@ public boolean isInitialized() { } public Boolean hasNode(DiscoveryNode node) { - if(nodes == null) { - if(log.isDebugEnabled()) { + 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; + return nodes.nodeExists(node) ? Boolean.TRUE : Boolean.FALSE; } } diff --git a/src/main/java/org/opensearch/security/configuration/CompatConfig.java b/src/main/java/org/opensearch/security/configuration/CompatConfig.java index 48f91b10be..ec2a521afe 100644 --- a/src/main/java/org/opensearch/security/configuration/CompatConfig.java +++ b/src/main/java/org/opensearch/security/configuration/CompatConfig.java @@ -57,12 +57,15 @@ public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { log.debug("dynamicSecurityConfig updated?: {}", (dcm != null)); } - //true is default + // true is default public boolean restAuthEnabled() { - final boolean restInitiallyDisabled = staticSettings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY, false); + final boolean restInitiallyDisabled = staticSettings.getAsBoolean( + ConfigConstants.SECURITY_UNSUPPORTED_DISABLE_REST_AUTH_INITIALLY, + false + ); final boolean isTraceEnabled = log.isTraceEnabled(); - if(restInitiallyDisabled) { - if(dcm == null) { + if (restInitiallyDisabled) { + if (dcm == null) { if (isTraceEnabled) { log.trace("dynamicSecurityConfig is null, initially static restDisabled"); } @@ -80,12 +83,15 @@ public boolean restAuthEnabled() { } - //true is default + // true is default public boolean transportInterClusterAuthEnabled() { - final boolean interClusterAuthInitiallyDisabled = staticSettings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY, false); + final boolean interClusterAuthInitiallyDisabled = staticSettings.getAsBoolean( + ConfigConstants.SECURITY_UNSUPPORTED_DISABLE_INTERTRANSPORT_AUTH_INITIALLY, + false + ); final boolean isTraceEnabled = log.isTraceEnabled(); - if(interClusterAuthInitiallyDisabled) { - if(dcm == null) { + if (interClusterAuthInitiallyDisabled) { + if (dcm == null) { if (isTraceEnabled) { log.trace("dynamicSecurityConfig is null, initially static interClusterAuthDisabled"); } @@ -107,7 +113,7 @@ public boolean transportInterClusterAuthEnabled() { */ public boolean transportInterClusterPassiveAuthEnabled() { final boolean interClusterAuthInitiallyPassive = transportPassiveAuthSetting.getDynamicSettingValue(); - if(log.isTraceEnabled()) { + if (log.isTraceEnabled()) { log.trace("{} {}", SECURITY_UNSUPPORTED_PASSIVE_INTERTRANSPORT_AUTH_INITIALLY, interClusterAuthInitiallyPassive); } return interClusterAuthInitiallyPassive; diff --git a/src/main/java/org/opensearch/security/configuration/ConfigCallback.java b/src/main/java/org/opensearch/security/configuration/ConfigCallback.java index cb8fc1eedf..a3b4cff90d 100644 --- a/src/main/java/org/opensearch/security/configuration/ConfigCallback.java +++ b/src/main/java/org/opensearch/security/configuration/ConfigCallback.java @@ -32,8 +32,11 @@ public interface ConfigCallback { void success(SecurityDynamicConfiguration dConf); + void noData(String id); + void singleFailure(Failure failure); + void failure(Throwable t); } diff --git a/src/main/java/org/opensearch/security/configuration/ConfigurationLoaderSecurity7.java b/src/main/java/org/opensearch/security/configuration/ConfigurationLoaderSecurity7.java index 3019c76462..f7802fd10e 100644 --- a/src/main/java/org/opensearch/security/configuration/ConfigurationLoaderSecurity7.java +++ b/src/main/java/org/opensearch/security/configuration/ConfigurationLoaderSecurity7.java @@ -78,7 +78,10 @@ public class ConfigurationLoaderSecurity7 { super(); this.client = client; this.settings = settings; - this.securityIndex = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + this.securityIndex = settings.get( + ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + ); this.cs = cs; log.debug("Index is: {}", securityIndex); } @@ -91,7 +94,8 @@ boolean isAuditConfigDocPresentInIndex() { return isAuditConfigDocPresentInIndex.get(); } - Map> load(final CType[] events, long timeout, TimeUnit timeUnit, boolean acceptInvalid) throws InterruptedException, TimeoutException { + Map> load(final CType[] events, long timeout, TimeUnit timeUnit, boolean acceptInvalid) + throws InterruptedException, TimeoutException { final CountDownLatch latch = new CountDownLatch(events.length); final Map> rs = new HashMap<>(events.length); final boolean isDebugEnabled = log.isDebugEnabled(); @@ -99,8 +103,13 @@ Map> load(final CType[] events, long time @Override public void success(SecurityDynamicConfiguration dConf) { - if(latch.getCount() <= 0) { - log.error("Latch already counted down (for {} of {}) (index={})", dConf.getCType().toLCString(), Arrays.toString(events), securityIndex); + if (latch.getCount() <= 0) { + log.error( + "Latch already counted down (for {} of {}) (index={})", + dConf.getCType().toLCString(), + Arrays.toString(events), + securityIndex + ); } // Audit configuration doc is available in the index. @@ -112,25 +121,39 @@ public void success(SecurityDynamicConfiguration dConf) { rs.put(dConf.getCType(), dConf); latch.countDown(); if (isDebugEnabled) { - log.debug("Received config for {} (of {}) with current latch value={}", dConf.getCType().toLCString(), Arrays.toString(events), latch.getCount()); + log.debug( + "Received config for {} (of {}) with current latch value={}", + dConf.getCType().toLCString(), + 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), securityIndex); + log.error( + "Failure {} retrieving configuration for {} (index={})", + failure == null ? null : failure.getMessage(), + Arrays.toString(events), + securityIndex + ); } @Override public void noData(String id) { CType cType = CType.fromString(id); - // Since NODESDN is newly introduced data-type applying for existing clusters as well, we make it backward compatible by returning valid empty + // Since NODESDN is newly introduced data-type applying for existing clusters as well, we make it backward compatible by + // returning valid empty // SecurityDynamicConfiguration. // Same idea for new setting WHITELIST/ALLOWLIST if (cType == CType.NODESDN || cType == CType.WHITELIST || cType == CType.ALLOWLIST) { try { - SecurityDynamicConfiguration empty = ConfigHelper.createEmptySdc(cType, ConfigurationRepository.getDefaultConfigVersion()); + SecurityDynamicConfiguration empty = ConfigHelper.createEmptySdc( + cType, + ConfigurationRepository.getDefaultConfigVersion() + ); rs.put(cType, empty); latch.countDown(); return; @@ -139,12 +162,15 @@ public void noData(String id) { } } - if(cType == CType.AUDIT) { + if (cType == CType.AUDIT) { // Audit configuration doc is not available in the index. // Configuration cannot be hot-reloaded. isAuditConfigDocPresentInIndex.set(false); try { - SecurityDynamicConfiguration empty = ConfigHelper.createEmptySdc(cType, ConfigurationRepository.getDefaultConfigVersion()); + SecurityDynamicConfiguration empty = ConfigHelper.createEmptySdc( + cType, + ConfigurationRepository.getDefaultConfigVersion() + ); empty.putCObject("config", AuditConfig.from(settings)); rs.put(cType, empty); latch.countDown(); @@ -163,16 +189,26 @@ public void failure(Throwable t) { } }, acceptInvalid); - if(!latch.await(timeout, timeUnit)) { - //timeout - throw new TimeoutException("Timeout after "+timeout+""+timeUnit+" while retrieving configuration for "+Arrays.toString(events)+ "(index="+securityIndex+")"); + if (!latch.await(timeout, timeUnit)) { + // timeout + throw new TimeoutException( + "Timeout after " + + timeout + + "" + + timeUnit + + " while retrieving configuration for " + + Arrays.toString(events) + + "(index=" + + securityIndex + + ")" + ); } return rs; } void loadAsync(final CType[] events, final ConfigCallback callback, boolean acceptInvalid) { - if(events == null || events.length == 0) { + if (events == null || events.length == 0) { log.warn("No config events requested to load"); return; } @@ -193,28 +229,28 @@ 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()) { + if (singleResponse != null && !singleResponse.isFailed()) { GetResponse singleGetResponse = singleResponse.getResponse(); - if(singleGetResponse.isExists() && !singleGetResponse.isSourceEmpty()) { - //success + if (singleGetResponse.isExists() && !singleGetResponse.isSourceEmpty()) { + // success try { final SecurityDynamicConfiguration dConf = toConfig(singleGetResponse, acceptInvalid); - if(dConf != null) { + if (dConf != null) { callback.success(dConf.deepClone()); } else { - callback.failure(new Exception("Cannot parse settings for "+singleGetResponse.getId())); + callback.failure(new Exception("Cannot parse settings for " + singleGetResponse.getId())); } } catch (Exception e) { log.error(e.toString()); callback.failure(e); } } else { - //does not exist or empty source + // does not exist or empty source callback.noData(singleGetResponse.getId()); } } else { - //failure - callback.singleFailure(singleResponse==null?null:singleResponse.getFailure()); + // failure + callback.singleFailure(singleResponse == null ? null : singleResponse.getFailure()); } } } @@ -233,8 +269,6 @@ private SecurityDynamicConfiguration toConfig(GetResponse singleGetResponse, final long seqNo = singleGetResponse.getSeqNo(); final long primaryTerm = singleGetResponse.getPrimaryTerm(); - - if (ref == null || ref.length() == 0) { log.error("Empty or null byte reference for {}", id); return null; @@ -247,7 +281,7 @@ private SecurityDynamicConfiguration toConfig(GetResponse singleGetResponse, parser.nextToken(); parser.nextToken(); - if(!id.equals((parser.currentName()))) { + if (!id.equals((parser.currentName()))) { log.error("Cannot parse config for type {} because {}!={}", id, id, parser.currentName()); return null; } @@ -258,35 +292,47 @@ private SecurityDynamicConfiguration toConfig(GetResponse singleGetResponse, final JsonNode jsonNode = DefaultObjectMapper.readTree(jsonAsString); int configVersion = 1; - - - if(jsonNode.get("_meta") != null) { + if (jsonNode.get("_meta") != null) { assert jsonNode.get("_meta").get("type").asText().equals(id); configVersion = jsonNode.get("_meta").get("config_version").asInt(); } - if(log.isDebugEnabled()) { - log.debug("Load "+id+" with version "+configVersion); + if (log.isDebugEnabled()) { + log.debug("Load " + id + " with version " + configVersion); } if (CType.ACTIONGROUPS.toLCString().equals(id)) { try { - return SecurityDynamicConfiguration.fromJson(jsonAsString, CType.fromString(id), configVersion, seqNo, primaryTerm, acceptInvalid); + return SecurityDynamicConfiguration.fromJson( + jsonAsString, + CType.fromString(id), + configVersion, + seqNo, + primaryTerm, + acceptInvalid + ); } catch (Exception e) { - if(log.isDebugEnabled()) { - log.debug("Unable to load "+id+" with version "+configVersion+" - Try loading legacy format ..."); + if (log.isDebugEnabled()) { + log.debug("Unable to load " + id + " with version " + configVersion + " - Try loading legacy format ..."); } return SecurityDynamicConfiguration.fromJson(jsonAsString, CType.fromString(id), 0, seqNo, primaryTerm, acceptInvalid); } } - return SecurityDynamicConfiguration.fromJson(jsonAsString, CType.fromString(id), configVersion, seqNo, primaryTerm, acceptInvalid); + return SecurityDynamicConfiguration.fromJson( + jsonAsString, + CType.fromString(id), + configVersion, + seqNo, + primaryTerm, + acceptInvalid + ); } finally { - if(parser != null) { + if (parser != null) { try { parser.close(); } catch (IOException e) { - //ignore + // ignore } } } diff --git a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java index 31e2a7d16f..92ae22af01 100644 --- a/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java +++ b/src/main/java/org/opensearch/security/configuration/ConfigurationRepository.java @@ -95,9 +95,18 @@ public class ConfigurationRepository { private final AtomicBoolean installDefaultConfig = new AtomicBoolean(); private final boolean acceptInvalid; - private ConfigurationRepository(Settings settings, final Path configPath, ThreadPool threadPool, - Client client, ClusterService clusterService, AuditLog auditLog) { - this.securityIndex = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + private ConfigurationRepository( + Settings settings, + final Path configPath, + ThreadPool threadPool, + Client client, + ClusterService clusterService, + AuditLog auditLog + ) { + this.securityIndex = settings.get( + ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + ); this.settings = settings; this.client = client; this.threadPool = threadPool; @@ -107,45 +116,90 @@ private ConfigurationRepository(Settings settings, final Path configPath, Thread this.acceptInvalid = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_ACCEPT_INVALID_CONFIG, false); cl = new ConfigurationLoaderSecurity7(client, threadPool, settings, clusterService); - configCache = CacheBuilder - .newBuilder() - .build(); + configCache = CacheBuilder.newBuilder().build(); bgThread = new Thread(() -> { try { - LOGGER.info("Background init thread started. Install default config?: "+installDefaultConfig.get()); + LOGGER.info("Background init thread started. Install default config?: " + installDefaultConfig.get()); // wait for the cluster here until it will finish managed node election while (clusterService.state().blocks().hasGlobalBlockWithStatus(RestStatus.SERVICE_UNAVAILABLE)) { LOGGER.info("Wait for cluster to be available ..."); TimeUnit.SECONDS.sleep(1); } - if(installDefaultConfig.get()) { + if (installDefaultConfig.get()) { try { String lookupDir = System.getProperty("security.default_init.dir"); - final String cd = lookupDir != null? (lookupDir+"/") : new Environment(settings, configPath).configDir().toAbsolutePath().toString()+"/opensearch-security/"; - File confFile = new File(cd+"config.yml"); - if(confFile.exists()) { + final String cd = lookupDir != null + ? (lookupDir + "/") + : new Environment(settings, configPath).configDir().toAbsolutePath().toString() + "/opensearch-security/"; + File confFile = new File(cd + "config.yml"); + if (confFile.exists()) { final ThreadContext threadContext = threadPool.getThreadContext(); - try(StoredContext ctx = threadContext.stashContext()) { + try (StoredContext ctx = threadContext.stashContext()) { threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); createSecurityIndexIfAbsent(); waitForSecurityIndexToBeAtLeastYellow(); - ConfigHelper.uploadFile(client, cd+"config.yml", securityIndex, CType.CONFIG, DEFAULT_CONFIG_VERSION); - ConfigHelper.uploadFile(client, cd+"roles.yml", securityIndex, CType.ROLES, DEFAULT_CONFIG_VERSION); - ConfigHelper.uploadFile(client, cd+"roles_mapping.yml", securityIndex, CType.ROLESMAPPING, DEFAULT_CONFIG_VERSION); - ConfigHelper.uploadFile(client, cd+"internal_users.yml", securityIndex, CType.INTERNALUSERS, DEFAULT_CONFIG_VERSION); - ConfigHelper.uploadFile(client, cd+"action_groups.yml", securityIndex, CType.ACTIONGROUPS, DEFAULT_CONFIG_VERSION); - if(DEFAULT_CONFIG_VERSION == 2) { - ConfigHelper.uploadFile(client, cd+"tenants.yml", securityIndex, CType.TENANTS, DEFAULT_CONFIG_VERSION); + ConfigHelper.uploadFile(client, cd + "config.yml", securityIndex, CType.CONFIG, DEFAULT_CONFIG_VERSION); + ConfigHelper.uploadFile(client, cd + "roles.yml", securityIndex, CType.ROLES, DEFAULT_CONFIG_VERSION); + ConfigHelper.uploadFile( + client, + cd + "roles_mapping.yml", + securityIndex, + CType.ROLESMAPPING, + DEFAULT_CONFIG_VERSION + ); + ConfigHelper.uploadFile( + client, + cd + "internal_users.yml", + securityIndex, + CType.INTERNALUSERS, + DEFAULT_CONFIG_VERSION + ); + ConfigHelper.uploadFile( + client, + cd + "action_groups.yml", + securityIndex, + CType.ACTIONGROUPS, + DEFAULT_CONFIG_VERSION + ); + if (DEFAULT_CONFIG_VERSION == 2) { + ConfigHelper.uploadFile( + client, + cd + "tenants.yml", + securityIndex, + CType.TENANTS, + DEFAULT_CONFIG_VERSION + ); } final boolean populateEmptyIfFileMissing = true; - ConfigHelper.uploadFile(client, cd+"nodes_dn.yml", securityIndex, CType.NODESDN, DEFAULT_CONFIG_VERSION, populateEmptyIfFileMissing); - ConfigHelper.uploadFile(client, cd + "whitelist.yml", securityIndex, CType.WHITELIST, DEFAULT_CONFIG_VERSION, populateEmptyIfFileMissing); - ConfigHelper.uploadFile(client, cd + "allowlist.yml", securityIndex, CType.ALLOWLIST, DEFAULT_CONFIG_VERSION, populateEmptyIfFileMissing); + ConfigHelper.uploadFile( + client, + cd + "nodes_dn.yml", + securityIndex, + CType.NODESDN, + DEFAULT_CONFIG_VERSION, + populateEmptyIfFileMissing + ); + ConfigHelper.uploadFile( + client, + cd + "whitelist.yml", + securityIndex, + CType.WHITELIST, + DEFAULT_CONFIG_VERSION, + populateEmptyIfFileMissing + ); + ConfigHelper.uploadFile( + client, + cd + "allowlist.yml", + securityIndex, + CType.ALLOWLIST, + DEFAULT_CONFIG_VERSION, + populateEmptyIfFileMissing + ); // audit.yml is not packaged by default final String auditConfigPath = cd + "audit.yml"; @@ -161,7 +215,7 @@ private ConfigurationRepository(Settings settings, final Path configPath, Thread } } - while(!dynamicConfigFactory.isInitialized()) { + while (!dynamicConfigFactory.isInitialized()) { try { LOGGER.debug("Try to load config ..."); reloadConfiguration(Arrays.asList(CType.values())); @@ -180,7 +234,10 @@ private ConfigurationRepository(Settings settings, final Path configPath, Thread final Set deprecatedAuditKeysInSettings = AuditConfig.getDeprecatedKeys(settings); if (!deprecatedAuditKeysInSettings.isEmpty()) { - LOGGER.warn("Following keys {} are deprecated in opensearch settings. They will be removed in plugin v2.0.0.0", deprecatedAuditKeysInSettings); + LOGGER.warn( + "Following keys {} are deprecated in opensearch settings. They will be removed in plugin v2.0.0.0", + deprecatedAuditKeysInSettings + ); } final boolean isAuditConfigDocPresentInIndex = cl.isAuditConfigDocPresentInIndex(); if (isAuditConfigDocPresentInIndex) { @@ -189,14 +246,16 @@ private ConfigurationRepository(Settings settings, final Path configPath, Thread } LOGGER.info("Hot-reloading of audit configuration is enabled"); } else { - LOGGER.info("Hot-reloading of audit configuration is disabled. Using configuration with defaults from opensearch settings. Populate the configuration in index using audit.yml or securityadmin to enable it."); + LOGGER.info( + "Hot-reloading of audit configuration is disabled. Using configuration with defaults from opensearch settings. Populate the configuration in index using audit.yml or securityadmin to enable it." + ); auditLog.setConfig(AuditConfig.from(settings)); } LOGGER.info("Node '{}' initialized", clusterService.localNode().getName()); } catch (Exception e) { - LOGGER.error("Unexpected exception while initializing node "+e, e); + LOGGER.error("Unexpected exception while initializing node " + e, e); } }); @@ -204,17 +263,9 @@ private ConfigurationRepository(Settings settings, final Path configPath, Thread private boolean createSecurityIndexIfAbsent() { try { - final Map indexSettings = ImmutableMap.of( - "index.number_of_shards", 1, - "index.auto_expand_replicas", "0-all" - ); - final CreateIndexRequest createIndexRequest = new CreateIndexRequest(securityIndex) - .settings(indexSettings); - final boolean ok = client.admin() - .indices() - .create(createIndexRequest) - .actionGet() - .isAcknowledged(); + final Map indexSettings = ImmutableMap.of("index.number_of_shards", 1, "index.auto_expand_replicas", "0-all"); + final CreateIndexRequest createIndexRequest = new CreateIndexRequest(securityIndex).settings(indexSettings); + final boolean ok = client.admin().indices().create(createIndexRequest).actionGet().isAcknowledged(); LOGGER.info("Index {} created?: {}", securityIndex, ok); return ok; } catch (ResourceAlreadyExistsException resourceAlreadyExistsException) { @@ -227,19 +278,24 @@ private void waitForSecurityIndexToBeAtLeastYellow() { LOGGER.info("Node started, try to initialize it. Wait for at least yellow cluster state...."); ClusterHealthResponse response = null; try { - response = client.admin().cluster().health(new ClusterHealthRequest(securityIndex) - .waitForActiveShards(1) - .waitForYellowStatus()).actionGet(); + response = client.admin() + .cluster() + .health(new ClusterHealthRequest(securityIndex).waitForActiveShards(1).waitForYellowStatus()) + .actionGet(); } catch (Exception e) { LOGGER.debug("Caught a {} but we just try again ...", e.toString()); } - while(response == null || response.isTimedOut() || response.getStatus() == ClusterHealthStatus.RED) { - LOGGER.debug("index '{}' not healthy yet, we try again ... (Reason: {})", securityIndex, response==null?"no response":(response.isTimedOut()?"timeout":"other, maybe red cluster")); + while (response == null || response.isTimedOut() || response.getStatus() == ClusterHealthStatus.RED) { + LOGGER.debug( + "index '{}' not healthy yet, we try again ... (Reason: {})", + securityIndex, + response == null ? "no response" : (response.isTimedOut() ? "timeout" : "other, maybe red cluster") + ); try { Thread.sleep(500); } catch (InterruptedException e) { - //ignore + // ignore Thread.currentThread().interrupt(); } try { @@ -256,13 +312,17 @@ public void initOnNodeStart() { LOGGER.info("Will attempt to create index {} and default configs if they are absent", securityIndex); installDefaultConfig.set(true); bgThread.start(); - } else if (settings.getAsBoolean(ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, true)){ - LOGGER.info("Will not attempt to create index {} and default configs if they are absent. Use securityadmin to initialize cluster", - securityIndex); + } else if (settings.getAsBoolean(ConfigConstants.SECURITY_BACKGROUND_INIT_IF_SECURITYINDEX_NOT_EXIST, true)) { + LOGGER.info( + "Will not attempt to create index {} and default configs if they are absent. Use securityadmin to initialize cluster", + securityIndex + ); bgThread.start(); } else { - LOGGER.info("Will not attempt to create index {} and default configs if they are absent. Will not perform background initialization", - securityIndex); + LOGGER.info( + "Will not attempt to create index {} and default configs if they are absent. Will not perform background initialization", + securityIndex + ); } } catch (Throwable e2) { LOGGER.error("Error during node initialization: {}", e2, e2); @@ -274,9 +334,22 @@ public boolean isAuditHotReloadingEnabled() { return cl.isAuditConfigDocPresentInIndex(); } - public static ConfigurationRepository create(Settings settings, final Path configPath, final ThreadPool threadPool, - Client client, ClusterService clusterService, AuditLog auditLog) { - final ConfigurationRepository repository = new ConfigurationRepository(settings, configPath, threadPool, client, clusterService, auditLog); + public static ConfigurationRepository create( + Settings settings, + final Path configPath, + final ThreadPool threadPool, + Client client, + ClusterService clusterService, + AuditLog auditLog + ) { + final ConfigurationRepository repository = new ConfigurationRepository( + settings, + configPath, + threadPool, + client, + clusterService, + auditLog + ); return repository; } @@ -290,8 +363,8 @@ public void setDynamicConfigFactory(DynamicConfigFactory dynamicConfigFactory) { * @return can also return empty in case it was never loaded */ public SecurityDynamicConfiguration getConfiguration(CType configurationType) { - SecurityDynamicConfiguration conf= configCache.getIfPresent(configurationType); - if(conf != null) { + SecurityDynamicConfiguration conf = configCache.getIfPresent(configurationType); + if (conf != null) { return conf.deepClone(); } return SecurityDynamicConfiguration.empty(); @@ -316,7 +389,6 @@ public void reloadConfiguration(Collection configTypes) throws ConfigUpda } } - private void reloadConfiguration0(Collection configTypes, boolean acceptInvalid) { final Map> loaded = getConfigurationsFromIndex(configTypes, false, acceptInvalid); configCache.putAll(loaded); @@ -333,7 +405,7 @@ private synchronized void notifyAboutChanges(Map> getConfigurationsFromIndex(Collection configTypes, boolean logComplianceEvent) { + public Map> getConfigurationsFromIndex( + Collection configTypes, + boolean logComplianceEvent + ) { return getConfigurationsFromIndex(configTypes, logComplianceEvent, this.acceptInvalid); } - public Map> getConfigurationsFromIndex(Collection configTypes, boolean logComplianceEvent, boolean acceptInvalid) { + public Map> getConfigurationsFromIndex( + Collection configTypes, + boolean logComplianceEvent, + boolean acceptInvalid + ) { final ThreadContext threadContext = threadPool.getThreadContext(); final Map> retVal = new HashMap<>(); - try(StoredContext ctx = threadContext.stashContext()) { + try (StoredContext ctx = threadContext.stashContext()) { threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); IndexMetadata securityMetadata = clusterService.state().metadata().index(this.securityIndex); - MappingMetadata mappingMetadata = securityMetadata==null?null:securityMetadata.mapping(); + MappingMetadata mappingMetadata = securityMetadata == null ? null : securityMetadata.mapping(); if (securityMetadata != null && mappingMetadata != null) { - if("security".equals(mappingMetadata.type())) { + if ("security".equals(mappingMetadata.type())) { LOGGER.debug("security index exists and was created before ES 7 (legacy layout)"); } else { LOGGER.debug("security index exists and was created with ES 7 (new layout)"); } - retVal.putAll(validate(cl.load(configTypes.toArray(new CType[0]), 10, TimeUnit.SECONDS, acceptInvalid), configTypes.size())); - + retVal.putAll( + validate(cl.load(configTypes.toArray(new CType[0]), 10, TimeUnit.SECONDS, acceptInvalid), configTypes.size()) + ); } else { - //wait (and use new layout) + // wait (and use new layout) LOGGER.debug("security index not exists (yet)"); - retVal.putAll(validate(cl.load(configTypes.toArray(new CType[0]), 10, TimeUnit.SECONDS, acceptInvalid), configTypes.size())); + retVal.putAll( + validate(cl.load(configTypes.toArray(new CType[0]), 10, TimeUnit.SECONDS, acceptInvalid), configTypes.size()) + ); } } catch (Exception e) { @@ -389,9 +471,10 @@ public Map> getConfigurationsFromIndex(Co return retVal; } - private Map> validate(Map> conf, int expectedSize) throws InvalidConfigException { + private Map> validate(Map> conf, int expectedSize) + throws InvalidConfigException { - if(conf == null || conf.size() != expectedSize) { + if (conf == null || conf.size() != expectedSize) { throw new InvalidConfigException("Retrieved only partial configuration"); } diff --git a/src/main/java/org/opensearch/security/configuration/DlsFilterLevelActionHandler.java b/src/main/java/org/opensearch/security/configuration/DlsFilterLevelActionHandler.java index 4bb27a6cbb..fa1c4989e0 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFilterLevelActionHandler.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFilterLevelActionHandler.java @@ -68,19 +68,32 @@ public class DlsFilterLevelActionHandler { private static final Logger log = LogManager.getLogger(DlsFilterLevelActionHandler.class); - private static final Function LOCAL_CLUSTER_ALIAS_GETTER = ReflectiveAttributeAccessors - .protectedObjectAttr("localClusterAlias", String.class); - - public static boolean handle(String action, ActionRequest request, ActionListener listener, EvaluatedDlsFlsConfig evaluatedDlsFlsConfig, - Resolved resolved, Client nodeClient, ClusterService clusterService, IndicesService indicesService, - IndexNameExpressionResolver resolver, DlsQueryParser dlsQueryParser, ThreadContext threadContext) { + private static final Function LOCAL_CLUSTER_ALIAS_GETTER = ReflectiveAttributeAccessors.protectedObjectAttr( + "localClusterAlias", + String.class + ); + + public static boolean handle( + String action, + ActionRequest request, + ActionListener listener, + EvaluatedDlsFlsConfig evaluatedDlsFlsConfig, + Resolved resolved, + Client nodeClient, + ClusterService clusterService, + IndicesService indicesService, + IndexNameExpressionResolver resolver, + DlsQueryParser dlsQueryParser, + ThreadContext threadContext + ) { if (threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_FILTER_LEVEL_DLS_DONE) != null) { return true; } - if (action.startsWith("cluster:") || action.startsWith("indices:admin/template/") - || action.startsWith("indices:admin/index_template/")) { + if (action.startsWith("cluster:") + || action.startsWith("indices:admin/template/") + || action.startsWith("indices:admin/index_template/")) { return true; } @@ -98,8 +111,19 @@ public static boolean handle(String action, ActionRequest request, ActionListene return true; } - return new DlsFilterLevelActionHandler(action, request, listener, evaluatedDlsFlsConfig, resolved, nodeClient, clusterService, indicesService, - resolver, dlsQueryParser, threadContext).handle(); + return new DlsFilterLevelActionHandler( + action, + request, + listener, + evaluatedDlsFlsConfig, + resolved, + nodeClient, + clusterService, + indicesService, + resolver, + dlsQueryParser, + threadContext + ).handle(); } private final String action; @@ -117,9 +141,19 @@ public static boolean handle(String action, ActionRequest request, ActionListene private BoolQueryBuilder filterLevelQueryBuilder; private DocumentAllowList documentAllowlist; - DlsFilterLevelActionHandler(String action, ActionRequest request, ActionListener listener, EvaluatedDlsFlsConfig evaluatedDlsFlsConfig, - Resolved resolved, Client nodeClient, ClusterService clusterService, IndicesService indicesService, - IndexNameExpressionResolver resolver, DlsQueryParser dlsQueryParser, ThreadContext threadContext) { + DlsFilterLevelActionHandler( + String action, + ActionRequest request, + ActionListener listener, + EvaluatedDlsFlsConfig evaluatedDlsFlsConfig, + Resolved resolved, + Client nodeClient, + ClusterService clusterService, + IndicesService indicesService, + IndexNameExpressionResolver resolver, + DlsQueryParser dlsQueryParser, + ThreadContext threadContext + ) { this.action = action; this.request = request; this.listener = listener; @@ -170,8 +204,11 @@ private boolean handle() { return handle((ClusterSearchShardsRequest) request, ctx); } else { log.error("Unsupported request type for filter level DLS: " + request); - listener.onFailure(new OpenSearchSecurityException( - "Unsupported request type for filter level DLS: " + action + "; " + request.getClass().getName())); + listener.onFailure( + new OpenSearchSecurityException( + "Unsupported request type for filter level DLS: " + action + "; " + request.getClass().getName() + ) + ); return false; } } @@ -230,7 +267,9 @@ private boolean handle(GetRequest getRequest, StoredContext ctx) { } SearchRequest searchRequest = new SearchRequest(getRequest.indices()); - BoolQueryBuilder query = QueryBuilders.boolQuery().must(QueryBuilders.idsQuery().addIds(getRequest.id())).must(filterLevelQueryBuilder); + BoolQueryBuilder query = QueryBuilders.boolQuery() + .must(QueryBuilders.idsQuery().addIds(getRequest.id())) + .must(filterLevelQueryBuilder); searchRequest.source(SearchSourceBuilder.searchSource().query(query)); nodeClient.search(searchRequest, new ActionListener() { @@ -247,8 +286,21 @@ public void onResponse(SearchResponse response) { if (hits == 1) { getListener.onResponse(new GetResponse(searchHitToGetResult(response.getHits().getAt(0)))); } else if (hits == 0) { - getListener.onResponse(new GetResponse(new GetResult(searchRequest.indices()[0], getRequest.id(), - SequenceNumbers.UNASSIGNED_SEQ_NO, SequenceNumbers.UNASSIGNED_PRIMARY_TERM, -1, false, null, null, null))); + getListener.onResponse( + new GetResponse( + new GetResult( + searchRequest.indices()[0], + getRequest.id(), + SequenceNumbers.UNASSIGNED_SEQ_NO, + SequenceNumbers.UNASSIGNED_PRIMARY_TERM, + -1, + false, + null, + null, + null + ) + ) + ); } else { log.error("Unexpected hit count " + hits + " in " + response); listener.onFailure(new OpenSearchSecurityException("Internal error when performing DLS")); @@ -274,8 +326,9 @@ private boolean handle(MultiGetRequest multiGetRequest, StoredContext ctx) { documentAllowlist.applyTo(threadContext); } - Map> idsGroupedByIndex = multiGetRequest.getItems().stream() - .collect(Collectors.groupingBy((item) -> item.index(), Collectors.mapping((item) -> item.id(), Collectors.toSet()))); + Map> idsGroupedByIndex = multiGetRequest.getItems() + .stream() + .collect(Collectors.groupingBy((item) -> item.index(), Collectors.mapping((item) -> item.id(), Collectors.toSet()))); Set indices = idsGroupedByIndex.keySet(); SearchRequest searchRequest = new SearchRequest(indices.toArray(new String[indices.size()])); @@ -283,14 +336,16 @@ private boolean handle(MultiGetRequest multiGetRequest, StoredContext ctx) { if (indices.size() == 1) { Set ids = idsGroupedByIndex.get(indices.iterator().next()); - query = QueryBuilders.boolQuery().must(QueryBuilders.idsQuery().addIds(ids.toArray(new String[ids.size()]))) - .must(filterLevelQueryBuilder); + query = QueryBuilders.boolQuery() + .must(QueryBuilders.idsQuery().addIds(ids.toArray(new String[ids.size()]))) + .must(filterLevelQueryBuilder); } else { BoolQueryBuilder mgetQuery = QueryBuilders.boolQuery().minimumShouldMatch(1); for (Map.Entry> entry : idsGroupedByIndex.entrySet()) { - BoolQueryBuilder indexQuery = QueryBuilders.boolQuery().must(QueryBuilders.termQuery("_index", entry.getKey())) - .must(QueryBuilders.idsQuery().addIds(entry.getValue().toArray(new String[entry.getValue().size()]))); + BoolQueryBuilder indexQuery = QueryBuilders.boolQuery() + .must(QueryBuilders.termQuery("_index", entry.getKey())) + .must(QueryBuilders.idsQuery().addIds(entry.getValue().toArray(new String[entry.getValue().size()]))); mgetQuery.should(indexQuery); } @@ -315,7 +370,9 @@ public void onResponse(SearchResponse response) { @SuppressWarnings("unchecked") ActionListener multiGetListener = (ActionListener) listener; - multiGetListener.onResponse(new MultiGetResponse(itemResponses.toArray(new MultiGetItemResponse[itemResponses.size()]))); + multiGetListener.onResponse( + new MultiGetResponse(itemResponses.toArray(new MultiGetItemResponse[itemResponses.size()])) + ); } catch (Exception e) { listener.onFailure(e); } @@ -332,8 +389,11 @@ public void onFailure(Exception e) { } private boolean handle(ClusterSearchShardsRequest request, StoredContext ctx) { - listener.onFailure(new OpenSearchSecurityException( - "Filter-level DLS via cross cluster search is not available for scrolling and minimize_roundtrips=true")); + listener.onFailure( + new OpenSearchSecurityException( + "Filter-level DLS via cross cluster search is not available for scrolling and minimize_roundtrips=true" + ) + ); return false; } @@ -373,9 +433,14 @@ private GetResult searchHitToGetResult(SearchHit hit) { } else { if (log.isWarnEnabled()) { - log.warn("Could not find IndexService for " + hit.getIndex() + "; assuming all fields as document fields." + log.warn( + "Could not find IndexService for " + + hit.getIndex() + + "; assuming all fields as document fields." + "This should not happen, however this should also not pose a big problem as ES mixes the fields again anyway.\n" - + "IndexMetadata: " + indexMetadata); + + "IndexMetadata: " + + indexMetadata + ); } documentFields = fields; @@ -383,8 +448,17 @@ private GetResult searchHitToGetResult(SearchHit hit) { } } - return new GetResult(hit.getIndex(), hit.getId(), hit.getSeqNo(), hit.getPrimaryTerm(), hit.getVersion(), true, hit.getSourceRef(), - documentFields, metadataFields); + return new GetResult( + hit.getIndex(), + hit.getId(), + hit.getSeqNo(), + hit.getPrimaryTerm(), + hit.getVersion(), + true, + hit.getSourceRef(), + documentFields, + metadataFields + ); } private boolean modifyQuery() throws IOException { @@ -441,11 +515,15 @@ private boolean modifyQuery(String localClusterAlias) throws IOException { dlsQueryBuilder.should(parsedDlsQuery); } else { // The original request referred to several indices. That's why we have to scope each query to the index it is meant for - dlsQueryBuilder.should(QueryBuilders.boolQuery().must(QueryBuilders.termQuery("_index", prefixedIndex)).must(parsedDlsQuery)); + dlsQueryBuilder.should( + QueryBuilders.boolQuery().must(QueryBuilders.termQuery("_index", prefixedIndex)).must(parsedDlsQuery) + ); } - Set queryBuilders = QueryBuilderTraverser.findAll(parsedDlsQuery, - (q) -> (q instanceof TermsQueryBuilder) && ((TermsQueryBuilder) q).termsLookup() != null); + Set queryBuilders = QueryBuilderTraverser.findAll( + parsedDlsQuery, + (q) -> (q instanceof TermsQueryBuilder) && ((TermsQueryBuilder) q).termsLookup() != null + ); for (QueryBuilder queryBuilder : queryBuilders) { TermsQueryBuilder termsQueryBuilder = (TermsQueryBuilder) queryBuilder; diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsFilterLeafReader.java b/src/main/java/org/opensearch/security/configuration/DlsFlsFilterLeafReader.java index d849e6d999..7100be35e5 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsFilterLeafReader.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsFilterLeafReader.java @@ -85,7 +85,7 @@ import org.opensearch.security.support.SecurityUtils; import org.opensearch.security.support.WildcardMatcher; -class DlsFlsFilterLeafReader extends SequentialStoredFieldsLeafReader { +class DlsFlsFilterLeafReader extends SequentialStoredFieldsLeafReader { private static final String KEYWORD = ".keyword"; private static final String[] EMPTY_STRING_ARRAY = new String[0]; @@ -108,11 +108,18 @@ class DlsFlsFilterLeafReader extends SequentialStoredFieldsLeafReader { private DlsGetEvaluator dge = null; - - DlsFlsFilterLeafReader(final LeafReader delegate, final Set includesExcludes, - final Query dlsQuery, final IndexService indexService, final ThreadContext threadContext, - final ClusterService clusterService, - final AuditLog auditlog, final Set maskedFields, final ShardId shardId, final Salt salt) { + DlsFlsFilterLeafReader( + final LeafReader delegate, + final Set includesExcludes, + final Query dlsQuery, + final IndexService indexService, + final ThreadContext threadContext, + final ClusterService clusterService, + final AuditLog auditlog, + final Set maskedFields, + final ShardId shardId, + final Salt salt + ) { super(delegate); maskFields = (maskedFields != null && maskedFields.size() > 0); @@ -199,8 +206,6 @@ class DlsFlsFilterLeafReader extends SequentialStoredFieldsLeafReader { System.arraycopy(fa, 0, tmp, 0, i); this.flsFieldInfos = new FieldInfos(tmp); - - } else { this.includesSet = null; this.excludesSet = null; @@ -221,9 +226,9 @@ private class DlsGetEvaluator { private final boolean hasDeletions; public DlsGetEvaluator(final Query dlsQuery, final LeafReader in, boolean applyDlsHere) throws IOException { - if(dlsQuery != null && applyDlsHere) { - //borrowed from Apache Lucene (Copyright Apache Software Foundation (ASF)) - //https://github.com/apache/lucene-solr/blob/branch_6_3/lucene/misc/src/java/org/apache/lucene/index/PKIndexSplitter.java + if (dlsQuery != null && applyDlsHere) { + // borrowed from Apache Lucene (Copyright Apache Software Foundation (ASF)) + // https://github.com/apache/lucene-solr/blob/branch_6_3/lucene/misc/src/java/org/apache/lucene/index/PKIndexSplitter.java final IndexSearcher searcher = new IndexSearcher(DlsFlsFilterLeafReader.this); searcher.setQueryCache(null); final Weight preserveWeight = searcher.createWeight(dlsQuery, ScoreMode.COMPLETE_NO_SCORES, 1f); @@ -253,7 +258,7 @@ public DlsGetEvaluator(final Query dlsQuery, final LeafReader in, boolean applyD hasDeletions = true; } else { - //no dls or handled in a different place + // no dls or handled in a different place liveBits = in.getLiveDocs(); numDocs = in.numDocs(); readerCacheHelper = in.getReaderCacheHelper(); @@ -261,7 +266,7 @@ public DlsGetEvaluator(final Query dlsQuery, final LeafReader in, boolean applyD } } - //return null means no hidden docs + // return null means no hidden docs public Bits getLiveDocs() { return liveBits; } @@ -288,19 +293,18 @@ private MaskedFieldsMap(Map maskedFieldsMap) { public static MaskedFieldsMap extractMaskedFields(boolean maskFields, Set maskedFields, final Salt salt) { if (maskFields) { - return new MaskedFieldsMap(maskedFields.stream() - .map(mf -> new MaskedField(mf, salt)) - .collect(ImmutableMap.toImmutableMap(mf -> WildcardMatcher.from(mf.getName()), Function.identity()))); + return new MaskedFieldsMap( + maskedFields.stream() + .map(mf -> new MaskedField(mf, salt)) + .collect(ImmutableMap.toImmutableMap(mf -> WildcardMatcher.from(mf.getName()), Function.identity())) + ); } else { return new MaskedFieldsMap(Collections.emptyMap()); } } public Optional getMaskedField(String fieldName) { - return maskedFieldsMap.entrySet().stream() - .filter(entry -> entry.getKey().test(fieldName)) - .map(Map.Entry::getValue) - .findFirst(); + return maskedFieldsMap.entrySet().stream().filter(entry -> entry.getKey().test(fieldName)).map(Map.Entry::getValue).findFirst(); } public boolean anyMatch(String fieldName) { @@ -311,7 +315,6 @@ public WildcardMatcher getMatcher() { return WildcardMatcher.from(maskedFieldsMap.keySet()); } - } private static class DlsFlsSubReaderWrapper extends FilterDirectoryReader.SubReaderWrapper { @@ -326,10 +329,17 @@ private static class DlsFlsSubReaderWrapper extends FilterDirectoryReader.SubRea private final ShardId shardId; private final Salt salt; - public DlsFlsSubReaderWrapper(final Set includes, final Query dlsQuery, - final IndexService indexService, final ThreadContext threadContext, - final ClusterService clusterService, - final AuditLog auditlog, final Set maskedFields, ShardId shardId, final Salt salt) { + public DlsFlsSubReaderWrapper( + final Set includes, + final Query dlsQuery, + final IndexService indexService, + final ThreadContext threadContext, + final ClusterService clusterService, + final AuditLog auditlog, + final Set maskedFields, + ShardId shardId, + final Salt salt + ) { this.includes = includes; this.dlsQuery = dlsQuery; this.indexService = indexService; @@ -343,7 +353,18 @@ public DlsFlsSubReaderWrapper(final Set includes, final Query dlsQuery, @Override public LeafReader wrap(final LeafReader reader) { - return new DlsFlsFilterLeafReader(reader, includes, dlsQuery, indexService, threadContext, clusterService, auditlog, maskedFields, shardId, salt); + return new DlsFlsFilterLeafReader( + reader, + includes, + dlsQuery, + indexService, + threadContext, + clusterService, + auditlog, + maskedFields, + shardId, + salt + ); } } @@ -360,11 +381,32 @@ static class DlsFlsDirectoryReader extends FilterDirectoryReader { private final ShardId shardId; private final Salt salt; - public DlsFlsDirectoryReader(final DirectoryReader in, final Set includes, final Query dlsQuery, - final IndexService indexService, final ThreadContext threadContext, - final ClusterService clusterService, - final AuditLog auditlog, final Set maskedFields, ShardId shardId, final Salt salt) throws IOException { - super(in, new DlsFlsSubReaderWrapper(includes, dlsQuery, indexService, threadContext, clusterService, auditlog, maskedFields, shardId, salt)); + public DlsFlsDirectoryReader( + final DirectoryReader in, + final Set includes, + final Query dlsQuery, + final IndexService indexService, + final ThreadContext threadContext, + final ClusterService clusterService, + final AuditLog auditlog, + final Set maskedFields, + ShardId shardId, + final Salt salt + ) throws IOException { + super( + in, + new DlsFlsSubReaderWrapper( + includes, + dlsQuery, + indexService, + threadContext, + clusterService, + auditlog, + maskedFields, + shardId, + salt + ) + ); this.includes = includes; this.dlsQuery = dlsQuery; this.indexService = indexService; @@ -378,7 +420,18 @@ public DlsFlsDirectoryReader(final DirectoryReader in, final Set include @Override protected DirectoryReader doWrapDirectoryReader(final DirectoryReader in) throws IOException { - return new DlsFlsDirectoryReader(in, includes, dlsQuery, indexService, threadContext, clusterService, auditlog, maskedFields, shardId, salt); + return new DlsFlsDirectoryReader( + in, + includes, + dlsQuery, + indexService, + threadContext, + clusterService, + auditlog, + maskedFields, + shardId, + salt + ); } @Override @@ -467,7 +520,7 @@ private boolean isFls(final BytesRef termAsFiledName) { private boolean isFls(final String name) { - if(!flsEnabled) { + if (!flsEnabled) { return true; } @@ -477,7 +530,7 @@ private boolean isFls(final String name) { @Override public FieldInfos getFieldInfos() { - if(!flsEnabled) { + if (!flsEnabled) { return in.getFieldInfos(); } @@ -487,8 +540,14 @@ public FieldInfos getFieldInfos() { private class ComplianceAwareStoredFieldVisitor extends StoredFieldVisitor { private final StoredFieldVisitor delegate; - private FieldReadCallback fieldReadCallback = - new FieldReadCallback(threadContext, indexService, clusterService, auditlog, maskedFieldsMap.getMatcher(), shardId); + private FieldReadCallback fieldReadCallback = new FieldReadCallback( + threadContext, + indexService, + clusterService, + auditlog, + maskedFieldsMap.getMatcher(), + shardId + ); public ComplianceAwareStoredFieldVisitor(final StoredFieldVisitor delegate) { super(); @@ -501,7 +560,6 @@ public void binaryField(final FieldInfo fieldInfo, final byte[] value) throws IO delegate.binaryField(fieldInfo, value); } - @Override public Status needsField(final FieldInfo fieldInfo) throws IOException { return delegate.needsField(fieldInfo); @@ -584,7 +642,6 @@ public void binaryField(final FieldInfo fieldInfo, final byte[] value) throws IO } } - @Override public Status needsField(final FieldInfo fieldInfo) throws IOException { return isFls(fieldInfo.name) ? delegate.needsField(fieldInfo) : Status.NO; @@ -640,7 +697,11 @@ public void binaryField(final FieldInfo fieldInfo, final byte[] value) throws IO if (fieldInfo.name.equals("_source")) { final BytesReference bytesRef = new BytesArray(value); - final Tuple> bytesRefTuple = XContentHelper.convertToMap(bytesRef, false, XContentType.JSON); + final Tuple> bytesRefTuple = XContentHelper.convertToMap( + bytesRef, + false, + XContentType.JSON + ); Map filteredSource = bytesRefTuple.v2(); MapUtils.deepTraverseMap(filteredSource, HASH_CB); final XContentBuilder xBuilder = XContentBuilder.builder(bytesRefTuple.v1().xContent()).map(filteredSource); @@ -650,7 +711,6 @@ public void binaryField(final FieldInfo fieldInfo, final byte[] value) throws IO } } - @Override public Status needsField(final FieldInfo fieldInfo) throws IOException { return delegate.needsField(fieldInfo); @@ -745,7 +805,7 @@ public Fields getTermVectors(final int docID) throws IOException { @Override public Iterator iterator() { - return Iterators. filter(fields.iterator(), input -> isFls(input)); + return Iterators.filter(fields.iterator(), input -> isFls(input)); } @Override @@ -781,7 +841,7 @@ private BinaryDocValues wrapBinaryDocValues(final String field, final BinaryDocV final MaskedFieldsMap maskedFieldsMap; - if (binaryDocValues != null && ((maskedFieldsMap=getRuntimeMaskedFieldInfo()) != null)) { + if (binaryDocValues != null && ((maskedFieldsMap = getRuntimeMaskedFieldInfo()) != null)) { final MaskedField mf = maskedFieldsMap.getMaskedField(handleKeyword(field)).orElse(null); if (mf != null) { @@ -822,7 +882,6 @@ public BytesRef binaryValue() throws IOException { return binaryDocValues; } - @Override public SortedDocValues getSortedDocValues(final String field) throws IOException { return isFls(field) ? wrapSortedDocValues(field, in.getSortedDocValues(field)) : null; @@ -832,7 +891,7 @@ private SortedDocValues wrapSortedDocValues(final String field, final SortedDocV final MaskedFieldsMap maskedFieldsMap; - if (sortedDocValues != null && (maskedFieldsMap=getRuntimeMaskedFieldInfo())!=null) { + if (sortedDocValues != null && (maskedFieldsMap = getRuntimeMaskedFieldInfo()) != null) { final MaskedField mf = maskedFieldsMap.getMaskedField(handleKeyword(field)).orElse(null); if (mf != null) { @@ -843,7 +902,6 @@ public int lookupTerm(BytesRef key) throws IOException { return sortedDocValues.lookupTerm(key); } - @Override public TermsEnum termsEnum() throws IOException { return new MaskedTermsEnum(sortedDocValues.termsEnum(), mf); @@ -913,7 +971,6 @@ private SortedSetDocValues wrapSortedSetDocValues(final String field, final Sort final MaskedFieldsMap maskedFieldsMap; - if (sortedSetDocValues != null && ((maskedFieldsMap = getRuntimeMaskedFieldInfo()) != null)) { MaskedField mf = maskedFieldsMap.getMaskedField(handleKeyword(field)).orElse(null); @@ -1002,16 +1059,16 @@ public Terms terms(String field) throws IOException { private Terms wrapTerms(final String field, Terms terms) throws IOException { - if(terms == null) { + if (terms == null) { return null; } MaskedFieldsMap maskedFieldInfo = getRuntimeMaskedFieldInfo(); - if(maskedFieldInfo != null && maskedFieldInfo.anyMatch(handleKeyword(field))) { + if (maskedFieldInfo != null && maskedFieldInfo.anyMatch(handleKeyword(field))) { return null; } - if("_field_names".equals(field)) { + if ("_field_names".equals(field)) { return new FilteredTerms(terms); } return terms; @@ -1024,8 +1081,8 @@ public FilteredTermsEnum(TermsEnum delegate) { @Override public BytesRef next() throws IOException { - //wind forward in the sequence of terms until we reached the end or we find a allowed term(=field name) - //so that calling this method never return a term which is not allowed by fls rules + // wind forward in the sequence of terms until we reached the end or we find a allowed term(=field name) + // so that calling this method never return a term which is not allowed by fls rules for (BytesRef nextBytesRef = in.next(); nextBytesRef != null; nextBytesRef = in.next()) { if (!isFls((nextBytesRef))) { continue; @@ -1038,20 +1095,20 @@ public BytesRef next() throws IOException { @Override public SeekStatus seekCeil(BytesRef text) throws IOException { - //Get the current seek status for a given term in the original sequence of terms + // Get the current seek status for a given term in the original sequence of terms final SeekStatus delegateStatus = in.seekCeil(text); - //So delegateStatus here is either FOUND or NOT_FOUND - //check if the current term (=field name) is allowed - //If so just return current seek status + // So delegateStatus here is either FOUND or NOT_FOUND + // check if the current term (=field name) is allowed + // If so just return current seek status if (delegateStatus != SeekStatus.END && isFls((in.term()))) { return delegateStatus; } else if (delegateStatus == SeekStatus.END) { - //If we hit the end just return END + // If we hit the end just return END return SeekStatus.END; } else { - //If we are not at the end and the current term (=field name) is not allowed just check if - //we are at the end of the (filtered) iterator + // If we are not at the end and the current term (=field name) is not allowed just check if + // we are at the end of the (filtered) iterator if (this.next() != null) { return SeekStatus.NOT_FOUND; } else { @@ -1060,7 +1117,6 @@ public SeekStatus seekCeil(BytesRef text) throws IOException { } } - @Override public boolean seekExact(BytesRef term) throws IOException { return isFls(term) && in.seekExact(term); @@ -1079,12 +1135,15 @@ public long ord() throws IOException { private final class FilteredTerms extends FilterTerms { - //According to - //https://www.elastic.co/guide/en/elasticsearch/reference/6.8/mapping-field-names-field.html - //"The _field_names field used to index the names of every field in a document that contains any value other than null" - //"For fields which have either doc_values or norm enabled the exists query will still be available but will not use the _field_names field." - //That means if a field has no doc values (which is always the case for an analyzed string) and no norms we need to strip the non allowed fls fields - //from the _field_names field. They are stored as terms, so we need to create a FilterTerms implementation which skips the terms (=field names)not allowed by fls + // According to + // https://www.elastic.co/guide/en/elasticsearch/reference/6.8/mapping-field-names-field.html + // "The _field_names field used to index the names of every field in a document that contains any value other than null" + // "For fields which have either doc_values or norm enabled the exists query will still be available but will not use the + // _field_names field." + // That means if a field has no doc values (which is always the case for an analyzed string) and no norms we need to strip the non + // allowed fls fields + // from the _field_names field. They are stored as terms, so we need to create a FilterTerms implementation which skips the terms + // (=field names)not allowed by fls public FilteredTerms(Terms delegate) throws IOException { super(delegate); @@ -1123,13 +1182,15 @@ public boolean hasDeletions() { @SuppressWarnings("unchecked") private MaskedFieldsMap getRuntimeMaskedFieldInfo() { - final Map> maskedFieldsMap = (Map>) HeaderHelper.deserializeSafeFromHeader(threadContext, - ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER); + final Map> maskedFieldsMap = (Map>) HeaderHelper.deserializeSafeFromHeader( + threadContext, + ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER + ); final String maskedEval = SecurityUtils.evalMap(maskedFieldsMap, indexService.index().getName()); - if(maskedEval != null) { + if (maskedEval != null) { final Set mf = maskedFieldsMap.get(maskedEval); - if(mf != null && !mf.isEmpty()) { + if (mf != null && !mf.isEmpty()) { return MaskedFieldsMap.extractMaskedFields(true, mf, salt); } @@ -1139,8 +1200,8 @@ private MaskedFieldsMap getRuntimeMaskedFieldInfo() { } private String handleKeyword(final String field) { - if(field != null && field.endsWith(KEYWORD)) { - return field.substring(0, field.length()-KEYWORD.length()); + if (field != null && field.endsWith(KEYWORD)) { + return field.substring(0, field.length() - KEYWORD.length()); } return field; } @@ -1158,7 +1219,7 @@ public MaskedTermsEnum(TermsEnum delegate, MaskedField mf) { @Override public BytesRef next() throws IOException { - return delegate.next(); //no masking here + return delegate.next(); // no masking here } @Override @@ -1223,7 +1284,6 @@ public TermState termState() throws IOException { } - private String getRuntimeActionName() { return (String) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ACTION_NAME); } @@ -1233,16 +1293,15 @@ private boolean isSuggest() { } private boolean applyDlsHere() { - if(isSuggest()) { - //we need to apply it here + if (isSuggest()) { + // we need to apply it here return true; } - final String action = getRuntimeActionName(); assert action != null; - //we need to apply here if it is not a search request - //(a get for example) + // we need to apply here if it is not a search request + // (a get for example) return !action.startsWith("indices:data/read/search"); } } diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsRequestValve.java b/src/main/java/org/opensearch/security/configuration/DlsFlsRequestValve.java index f5751efcae..9bce6564dc 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsRequestValve.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsRequestValve.java @@ -37,7 +37,13 @@ public interface DlsFlsRequestValve { - boolean invoke(String action, ActionRequest request, ActionListener listener, EvaluatedDlsFlsConfig evaluatedDlsFlsConfig, Resolved resolved); + boolean invoke( + String action, + ActionRequest request, + ActionListener listener, + EvaluatedDlsFlsConfig evaluatedDlsFlsConfig, + Resolved resolved + ); void handleSearchContext(SearchContext context, ThreadPool threadPool, NamedXContentRegistry namedXContentRegistry); @@ -45,9 +51,14 @@ public interface DlsFlsRequestValve { public static class NoopDlsFlsRequestValve implements DlsFlsRequestValve { - @Override - public boolean invoke(String action, ActionRequest request, ActionListener listener, EvaluatedDlsFlsConfig evaluatedDlsFlsConfig, - Resolved resolved) { + @Override + public boolean invoke( + String action, + ActionRequest request, + ActionListener listener, + EvaluatedDlsFlsConfig evaluatedDlsFlsConfig, + Resolved resolved + ) { return true; } diff --git a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java index 947557d342..81fe9e255f 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java +++ b/src/main/java/org/opensearch/security/configuration/DlsFlsValveImpl.java @@ -82,8 +82,8 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve { - private static final String MAP_EXECUTION_HINT = "map"; - private static final Logger log = LogManager.getLogger(DlsFlsValveImpl.class); + private static final String MAP_EXECUTION_HINT = "map"; + private static final Logger log = LogManager.getLogger(DlsFlsValveImpl.class); private final Client nodeClient; private final ClusterService clusterService; @@ -92,8 +92,14 @@ public class DlsFlsValveImpl implements DlsFlsRequestValve { private final DlsQueryParser dlsQueryParser; private final IndexNameExpressionResolver resolver; - public DlsFlsValveImpl(Settings settings, Client nodeClient, ClusterService clusterService, IndexNameExpressionResolver resolver, - NamedXContentRegistry namedXContentRegistry, ThreadContext threadContext) { + public DlsFlsValveImpl( + Settings settings, + Client nodeClient, + ClusterService clusterService, + IndexNameExpressionResolver resolver, + NamedXContentRegistry namedXContentRegistry, + ThreadContext threadContext + ) { super(); this.nodeClient = nodeClient; this.clusterService = clusterService; @@ -109,12 +115,25 @@ public DlsFlsValveImpl(Settings settings, Client nodeClient, ClusterService clus * @param listener * @return false on error */ - public boolean invoke(String action, ActionRequest request, final ActionListener listener, EvaluatedDlsFlsConfig evaluatedDlsFlsConfig, - final Resolved resolved) { + public boolean invoke( + String action, + ActionRequest request, + final ActionListener listener, + EvaluatedDlsFlsConfig evaluatedDlsFlsConfig, + final Resolved resolved + ) { if (log.isDebugEnabled()) { - log.debug("DlsFlsValveImpl.invoke()\nrequest: " + request + "\nevaluatedDlsFlsConfig: " + evaluatedDlsFlsConfig + "\nresolved: " - + resolved + "\nmode: " + mode); + log.debug( + "DlsFlsValveImpl.invoke()\nrequest: " + + request + + "\nevaluatedDlsFlsConfig: " + + evaluatedDlsFlsConfig + + "\nresolved: " + + resolved + + "\nmode: " + + mode + ); } if (evaluatedDlsFlsConfig == null || evaluatedDlsFlsConfig.isEmpty()) { @@ -173,10 +192,10 @@ public boolean invoke(String action, ActionRequest request, final ActionListener SearchRequest searchRequest = ((SearchRequest) request); - //When we encounter a terms or sampler aggregation with masked fields activated we forcibly - //need to switch off global ordinals because field masking can break ordering + // When we encounter a terms or sampler aggregation with masked fields activated we forcibly + // need to switch off global ordinals because field masking can break ordering // CS-SUPPRESS-SINGLE: RegexpSingleline Ignore term inside of url - //https://www.elastic.co/guide/en/elasticsearch/reference/master/eager-global-ordinals.html#_avoiding_global_ordinal_loading + // https://www.elastic.co/guide/en/elasticsearch/reference/master/eager-global-ordinals.html#_avoiding_global_ordinal_loading // CS-ENFORCE-SINGLE if (evaluatedDlsFlsConfig.hasFieldMasking()) { @@ -197,8 +216,7 @@ public boolean invoke(String action, ActionRequest request, final ActionListener } } - if (!evaluatedDlsFlsConfig.hasFls() && !evaluatedDlsFlsConfig.hasDls() - && searchRequest.source().aggregations() != null) { + if (!evaluatedDlsFlsConfig.hasFls() && !evaluatedDlsFlsConfig.hasDls() && searchRequest.source().aggregations() != null) { boolean cacheable = true; @@ -224,8 +242,11 @@ public boolean invoke(String action, ActionRequest request, final ActionListener if (!cacheable) { searchRequest.requestCache(Boolean.FALSE); } else { - LogManager.getLogger("debuglogger").error("Shard requestcache enabled for " - + (searchRequest.source() == null ? "" : Strings.toString(XContentType.JSON, searchRequest.source()))); + LogManager.getLogger("debuglogger") + .error( + "Shard requestcache enabled for " + + (searchRequest.source() == null ? "" : Strings.toString(XContentType.JSON, searchRequest.source())) + ); } } else { @@ -241,7 +262,9 @@ public boolean invoke(String action, ActionRequest request, final ActionListener if (request instanceof BulkRequest) { for (DocWriteRequest inner : ((BulkRequest) request).requests()) { if (inner instanceof UpdateRequest) { - listener.onFailure(new OpenSearchSecurityException("Update is not supported when FLS or DLS or Fieldmasking is activated")); + listener.onFailure( + new OpenSearchSecurityException("Update is not supported when FLS or DLS or Fieldmasking is activated") + ); return false; } } @@ -250,7 +273,9 @@ public boolean invoke(String action, ActionRequest request, final ActionListener if (request instanceof BulkShardRequest) { for (BulkItemRequest inner : ((BulkShardRequest) request).items()) { if (inner.request() instanceof UpdateRequest) { - listener.onFailure(new OpenSearchSecurityException("Update is not supported when FLS or DLS or Fieldmasking is activated")); + listener.onFailure( + new OpenSearchSecurityException("Update is not supported when FLS or DLS or Fieldmasking is activated") + ); return false; } } @@ -261,9 +286,13 @@ public boolean invoke(String action, ActionRequest request, final ActionListener return false; } - if(action.contains("plugins/replication")) { - listener.onFailure(new OpenSearchSecurityException("Cross Cluster Replication is not supported when FLS or DLS or Fieldmasking is activated", - RestStatus.FORBIDDEN)); + if (action.contains("plugins/replication")) { + listener.onFailure( + new OpenSearchSecurityException( + "Cross Cluster Replication is not supported when FLS or DLS or Fieldmasking is activated", + RestStatus.FORBIDDEN + ) + ); return false; } @@ -292,8 +321,19 @@ public boolean invoke(String action, ActionRequest request, final ActionListener } if (doFilterLevelDls && filteredDlsFlsConfig.hasDls()) { - return DlsFilterLevelActionHandler.handle(action, request, listener, evaluatedDlsFlsConfig, resolved, nodeClient, clusterService, - OpenSearchSecurityPlugin.GuiceHolder.getIndicesService(), resolver, dlsQueryParser, threadContext); + return DlsFilterLevelActionHandler.handle( + action, + request, + listener, + evaluatedDlsFlsConfig, + resolved, + nodeClient, + clusterService, + OpenSearchSecurityPlugin.GuiceHolder.getIndicesService(), + resolver, + dlsQueryParser, + threadContext + ); } else { return true; } @@ -303,8 +343,10 @@ public boolean invoke(String action, ActionRequest request, final ActionListener public void handleSearchContext(SearchContext context, ThreadPool threadPool, NamedXContentRegistry namedXContentRegistry) { try { @SuppressWarnings("unchecked") - final Map> queries = (Map>) HeaderHelper.deserializeSafeFromHeader(threadPool.getThreadContext(), - ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER); + final Map> queries = (Map>) HeaderHelper.deserializeSafeFromHeader( + threadPool.getThreadContext(), + ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER + ); final String dlsEval = SecurityUtils.evalMap(queries, context.indexShard().indexSettings().getIndex().getName()); @@ -319,8 +361,11 @@ public void handleSearchContext(SearchContext context, ThreadPool threadPool, Na final Set unparsedDlsQueries = queries.get(dlsEval); if (unparsedDlsQueries != null && !unparsedDlsQueries.isEmpty()) { - BooleanQuery.Builder queryBuilder = dlsQueryParser.parse(unparsedDlsQueries, context.getQueryShardContext(), - (q) -> new ConstantScoreQuery(q)); + BooleanQuery.Builder queryBuilder = dlsQueryParser.parse( + unparsedDlsQueries, + context.getQueryShardContext(), + (q) -> new ConstantScoreQuery(q) + ); queryBuilder.add(context.parsedQuery().query(), Occur.MUST); @@ -343,11 +388,11 @@ public void onQueryPhase(QuerySearchResult queryResult) { assert aggregations != null; queryResult.aggregations( - InternalAggregations.from( - StreamSupport.stream(aggregations.spliterator(), false) - .map(aggregation -> aggregateBuckets((InternalAggregation)aggregation)) - .collect(ImmutableList.toImmutableList()) - ) + InternalAggregations.from( + StreamSupport.stream(aggregations.spliterator(), false) + .map(aggregation -> aggregateBuckets((InternalAggregation) aggregation)) + .collect(ImmutableList.toImmutableList()) + ) ); } @@ -363,7 +408,10 @@ private static InternalAggregation aggregateBuckets(InternalAggregation aggregat return aggregation; } - private static List mergeBuckets(List buckets, Comparator comparator) { + private static List mergeBuckets( + List buckets, + Comparator comparator + ) { if (log.isDebugEnabled()) { log.debug("Merging buckets: {}", buckets.stream().map(b -> b.getKeyAsString()).collect(ImmutableList.toImmutableList())); } @@ -383,18 +431,28 @@ private void setDlsHeaders(EvaluatedDlsFlsConfig dlsFls, ActionRequest request) Map> dlsQueries = dlsFls.getDlsQueriesByIndex(); if (request instanceof ClusterSearchShardsRequest && HeaderHelper.isTrustedClusterRequest(threadContext)) { - threadContext.addResponseHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER, Base64Helper.serializeObject((Serializable) dlsQueries)); + threadContext.addResponseHeader( + ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER, + Base64Helper.serializeObject((Serializable) dlsQueries) + ); if (log.isDebugEnabled()) { log.debug("added response header for DLS info: {}", dlsQueries); } } else { if (threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER) != null) { - Object deserializedDlsQueries = Base64Helper.deserializeObject(threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER)); + Object deserializedDlsQueries = Base64Helper.deserializeObject( + threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER) + ); if (!dlsQueries.equals(deserializedDlsQueries)) { - throw new OpenSearchSecurityException(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER + " does not match (SG 900D)"); + throw new OpenSearchSecurityException( + ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER + " does not match (SG 900D)" + ); } } else { - threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER, Base64Helper.serializeObject((Serializable) dlsQueries)); + threadContext.putHeader( + ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER, + Base64Helper.serializeObject((Serializable) dlsQueries) + ); if (log.isDebugEnabled()) { log.debug("attach DLS info: {}", dlsQueries); } @@ -408,7 +466,12 @@ private void setDlsModeHeader(Mode mode) { if (threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_MODE_HEADER) != null) { if (!modeString.equals(threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_MODE_HEADER))) { - log.warn("Cannot update DLS mode to " + mode + "; current: " + threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_MODE_HEADER)); + log.warn( + "Cannot update DLS mode to " + + mode + + "; current: " + + threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_MODE_HEADER) + ); } } else { threadContext.putHeader(ConfigConstants.OPENDISTRO_SECURITY_DLS_MODE_HEADER, modeString); @@ -430,22 +493,32 @@ private void setFlsHeaders(EvaluatedDlsFlsConfig dlsFls, ActionRequest request) Map> maskedFieldsMap = dlsFls.getFieldMaskingByIndex(); if (request instanceof ClusterSearchShardsRequest && HeaderHelper.isTrustedClusterRequest(threadContext)) { - threadContext.addResponseHeader(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER, Base64Helper.serializeObject((Serializable) maskedFieldsMap)); + threadContext.addResponseHeader( + ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER, + Base64Helper.serializeObject((Serializable) maskedFieldsMap) + ); if (log.isDebugEnabled()) { log.debug("added response header for masked fields info: {}", maskedFieldsMap); } } else { 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 OpenSearchSecurityException(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER + " does not match (SG 901D)"); + if (!maskedFieldsMap.equals( + Base64Helper.deserializeObject(threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER)) + )) { + throw new OpenSearchSecurityException( + ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER + " does not match (SG 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)); + threadContext.putHeader( + ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER, + Base64Helper.serializeObject((Serializable) maskedFieldsMap) + ); if (log.isDebugEnabled()) { log.debug("attach masked fields info: {}", maskedFieldsMap); } @@ -457,22 +530,37 @@ private void setFlsHeaders(EvaluatedDlsFlsConfig dlsFls, ActionRequest request) Map> flsFields = dlsFls.getFlsByIndex(); if (request instanceof ClusterSearchShardsRequest && HeaderHelper.isTrustedClusterRequest(threadContext)) { - threadContext.addResponseHeader(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER, Base64Helper.serializeObject((Serializable) flsFields)); + threadContext.addResponseHeader( + ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER, + Base64Helper.serializeObject((Serializable) flsFields) + ); if (log.isDebugEnabled()) { log.debug("added response header for FLS info: {}", flsFields); } } else { 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 OpenSearchSecurityException(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER + " does not match (SG 901D) " + flsFields - + "---" + Base64Helper.deserializeObject(threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER))); + if (!flsFields.equals( + Base64Helper.deserializeObject(threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER)) + )) { + throw new OpenSearchSecurityException( + ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER + + " does not match (SG 901D) " + + flsFields + + "---" + + Base64Helper.deserializeObject( + threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER) + ) + ); } 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)); + threadContext.putHeader( + ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER, + Base64Helper.serializeObject((Serializable) flsFields) + ); if (log.isDebugEnabled()) { log.debug("attach FLS info: {}", flsFields); } @@ -500,9 +588,16 @@ private void finalizeBucket() { if (mergeCount == 1) { builder.add(this.bucket); } else { - builder.add(new StringTerms.Bucket(StringTermsGetter.getTerm(bucket), mergedDocCount, - (InternalAggregations) bucket.getAggregations(), showDocCountError, mergedDocCountError, - StringTermsGetter.getDocValueFormat(bucket))); + builder.add( + new StringTerms.Bucket( + StringTermsGetter.getTerm(bucket), + mergedDocCount, + (InternalAggregations) bucket.getAggregations(), + showDocCountError, + mergedDocCountError, + StringTermsGetter.getDocValueFormat(bucket) + ) + ); } } @@ -543,8 +638,7 @@ private static class StringTermsGetter { private static final Field TERM_BYTES = getField(StringTerms.Bucket.class, "termBytes"); private static final Field FORMAT = getField(InternalTerms.Bucket.class, "format"); - private StringTermsGetter() { - } + private StringTermsGetter() {} private static Field getFieldPrivileged(Class cls, String name) { try { @@ -569,7 +663,7 @@ private static Field getField(Class cls, String name) { private static T getFieldValue(Field field, C c) { try { - return (T)field.get(c); + return (T) field.get(c); } catch (IllegalArgumentException | IllegalAccessException e) { log.error("Exception while getting value {} of class {}", field.getName(), c.getClass().getSimpleName(), e); if (e instanceof RuntimeException) { @@ -594,7 +688,9 @@ public static DocValueFormat getDocValueFormat(StringTerms.Bucket bucket) { } public static enum Mode { - ADAPTIVE, LUCENE_LEVEL, FILTER_LEVEL; + ADAPTIVE, + LUCENE_LEVEL, + FILTER_LEVEL; static Mode get(Settings settings) { String modeString = settings.get(ConfigConstants.SECURITY_DLS_MODE); diff --git a/src/main/java/org/opensearch/security/configuration/DlsQueryParser.java b/src/main/java/org/opensearch/security/configuration/DlsQueryParser.java index a5f07541c8..9640abcd8e 100644 --- a/src/main/java/org/opensearch/security/configuration/DlsQueryParser.java +++ b/src/main/java/org/opensearch/security/configuration/DlsQueryParser.java @@ -41,24 +41,28 @@ import org.opensearch.index.query.TermsQueryBuilder; import org.opensearch.security.queries.QueryBuilderTraverser; - public final class DlsQueryParser { private static final Logger log = LogManager.getLogger(DlsQueryParser.class); private static final Query NON_NESTED_QUERY; static { - //Match all documents but not the nested ones - //Nested document types start with __ - //https://discuss.elastic.co/t/whats-nested-documents-layout-inside-the-lucene/59944/9 + // Match all documents but not the nested ones + // Nested document types start with __ + // https://discuss.elastic.co/t/whats-nested-documents-layout-inside-the-lucene/59944/9 NON_NESTED_QUERY = new BooleanQuery.Builder().add(new MatchAllDocsQuery(), Occur.FILTER) - .add(new PrefixQuery(new Term("_type", "__")), Occur.MUST_NOT).build(); + .add(new PrefixQuery(new Term("_type", "__")), Occur.MUST_NOT) + .build(); } - private static Cache parsedQueryCache = CacheBuilder.newBuilder().maximumSize(10000).expireAfterWrite(4, TimeUnit.HOURS) - .build(); - private static Cache queryContainsTlqCache = CacheBuilder.newBuilder().maximumSize(10000).expireAfterWrite(4, TimeUnit.HOURS) - .build(); + private static Cache parsedQueryCache = CacheBuilder.newBuilder() + .maximumSize(10000) + .expireAfterWrite(4, TimeUnit.HOURS) + .build(); + private static Cache queryContainsTlqCache = CacheBuilder.newBuilder() + .maximumSize(10000) + .expireAfterWrite(4, TimeUnit.HOURS) + .build(); private final NamedXContentRegistry namedXContentRegistry; @@ -70,8 +74,11 @@ public BooleanQuery.Builder parse(Set unparsedDlsQueries, QueryShardCont return parse(unparsedDlsQueries, queryShardContext, null); } - public BooleanQuery.Builder parse(Set unparsedDlsQueries, QueryShardContext queryShardContext, - Function queryMapFunction) { + public BooleanQuery.Builder parse( + Set unparsedDlsQueries, + QueryShardContext queryShardContext, + Function queryMapFunction + ) { if (unparsedDlsQueries == null || unparsedDlsQueries.isEmpty()) { return null; @@ -100,8 +107,11 @@ public BooleanQuery.Builder parse(Set unparsedDlsQueries, QueryShardCont return dlsQueryBuilder; } - private static void handleNested(final QueryShardContext queryShardContext, final BooleanQuery.Builder dlsQueryBuilder, - final Query parentQuery) { + private static void handleNested( + final QueryShardContext queryShardContext, + final BooleanQuery.Builder dlsQueryBuilder, + final Query parentQuery + ) { final BitSetProducer parentDocumentsFilter = queryShardContext.bitsetFilter(NON_NESTED_QUERY); dlsQueryBuilder.add(new ToChildBlockJoinQuery(parentQuery, parentDocumentsFilter), Occur.SHOULD); } @@ -112,8 +122,11 @@ public QueryBuilder parse(String unparsedDlsQuery) { @Override public QueryBuilder call() throws Exception { - final XContentParser parser = JsonXContent.jsonXContent.createParser(namedXContentRegistry, - DeprecationHandler.THROW_UNSUPPORTED_OPERATION, unparsedDlsQuery); + final XContentParser parser = JsonXContent.jsonXContent.createParser( + namedXContentRegistry, + DeprecationHandler.THROW_UNSUPPORTED_OPERATION, + unparsedDlsQuery + ); return AbstractQueryBuilder.parseInnerQueryBuilder(parser); } @@ -143,18 +156,19 @@ boolean containsTermLookupQuery(Set unparsedQueries) { return false; } - boolean containsTermLookupQuery(String query) { + boolean containsTermLookupQuery(String query) { try { return queryContainsTlqCache.get(query, () -> { QueryBuilder queryBuilder = parse(query); - return QueryBuilderTraverser.exists(queryBuilder, - (q) -> (q instanceof TermsQueryBuilder) && ((TermsQueryBuilder) q).termsLookup() != null); + return QueryBuilderTraverser.exists( + queryBuilder, + (q) -> (q instanceof TermsQueryBuilder) && ((TermsQueryBuilder) q).termsLookup() != null + ); }); } catch (ExecutionException e) { throw new RuntimeException("Error handling parsing " + query, e.getCause()); } } - } diff --git a/src/main/java/org/opensearch/security/configuration/EmptyFilterLeafReader.java b/src/main/java/org/opensearch/security/configuration/EmptyFilterLeafReader.java index 79069ef53e..1a0460b91c 100644 --- a/src/main/java/org/opensearch/security/configuration/EmptyFilterLeafReader.java +++ b/src/main/java/org/opensearch/security/configuration/EmptyFilterLeafReader.java @@ -83,6 +83,7 @@ public CacheHelper getCoreCacheHelper() { public CacheHelper getReaderCacheHelper() { return null; } + private static class EmptySubReaderWrapper extends FilterDirectoryReader.SubReaderWrapper { @Override diff --git a/src/main/java/org/opensearch/security/configuration/MaskedField.java b/src/main/java/org/opensearch/security/configuration/MaskedField.java index c04567eeef..8cb20ccdfe 100644 --- a/src/main/java/org/opensearch/security/configuration/MaskedField.java +++ b/src/main/java/org/opensearch/security/configuration/MaskedField.java @@ -40,11 +40,11 @@ public MaskedField(final String value, final Salt salt) { } else if (tokenCount == 2) { name = tokens.get(0); algo = tokens.get(1); - } else if (tokenCount >= 3 && tokenCount%2==1) { + } else if (tokenCount >= 3 && tokenCount % 2 == 1) { name = tokens.get(0); - regexReplacements = new ArrayList<>((tokenCount-1)/2); - for(int i=1; i((tokenCount - 1) / 2); + for (int i = 1; i < tokenCount - 1; i = i + 2) { + regexReplacements.add(new RegexReplacement(tokens.get(i), tokens.get(i + 1))); } } else { throw new IllegalArgumentException("Expected 1 or 2 or >=3 (but then odd count) tokens, got " + tokenCount); @@ -52,7 +52,7 @@ public MaskedField(final String value, final Salt salt) { } public final void isValid() throws Exception { - mask(new byte[] {1,2,3,4,5}); + mask(new byte[] { 1, 2, 3, 4, 5 }); } public byte[] mask(byte[] value) { @@ -72,7 +72,7 @@ public String mask(String value) { } public BytesRef mask(BytesRef value) { - if(value == null) { + if (value == null) { return null; } @@ -87,8 +87,6 @@ public String getName() { return name; } - - @Override public int hashCode() { final int prime = 31; @@ -101,37 +99,35 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; MaskedField other = (MaskedField) obj; if (algo == null) { - if (other.algo != null) - return false; - } else if (!algo.equals(other.algo)) - return false; + if (other.algo != null) return false; + } else if (!algo.equals(other.algo)) return false; if (name == null) { - if (other.name != null) - return false; - } else if (!name.equals(other.name)) - return false; + if (other.name != null) return false; + } else if (!name.equals(other.name)) return false; if (regexReplacements == null) { - if (other.regexReplacements != null) - return false; - } else if (!regexReplacements.equals(other.regexReplacements)) - return false; + if (other.regexReplacements != null) return false; + } else if (!regexReplacements.equals(other.regexReplacements)) return false; return true; } - - @Override public String toString() { - return "MaskedField [name=" + name + ", algo=" + algo + ", regexReplacements=" + regexReplacements - + ", defaultSalt=" + Arrays.toString(defaultSalt) + ", isDefault()=" + isDefault() + "]"; + return "MaskedField [name=" + + name + + ", algo=" + + algo + + ", regexReplacements=" + + regexReplacements + + ", defaultSalt=" + + Arrays.toString(defaultSalt) + + ", isDefault()=" + + isDefault() + + "]"; } private boolean isDefault() { @@ -148,7 +144,7 @@ private byte[] customHash(byte[] in) { } } else if (regexReplacements != null) { String cur = new String(in, StandardCharsets.UTF_8); - for(RegexReplacement rr: regexReplacements) { + for (RegexReplacement rr : regexReplacements) { cur = cur.replaceAll(rr.getRegex(), rr.getReplacement()); } return cur.getBytes(StandardCharsets.UTF_8); @@ -190,7 +186,7 @@ private static class RegexReplacement { public RegexReplacement(String regex, String replacement) { super(); - this.regex = regex.substring(1).substring(0, regex.length()-2); + this.regex = regex.substring(1).substring(0, regex.length() - 2); this.replacement = replacement; } @@ -213,23 +209,16 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; RegexReplacement other = (RegexReplacement) obj; if (regex == null) { - if (other.regex != null) - return false; - } else if (!regex.equals(other.regex)) - return false; + if (other.regex != null) return false; + } else if (!regex.equals(other.regex)) return false; if (replacement == null) { - if (other.replacement != null) - return false; - } else if (!replacement.equals(other.replacement)) - return false; + if (other.replacement != null) return false; + } else if (!replacement.equals(other.replacement)) return false; return true; } diff --git a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java index e2f10dfcae..3f7b4737bf 100644 --- a/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java +++ b/src/main/java/org/opensearch/security/configuration/PrivilegesInterceptorImpl.java @@ -56,18 +56,30 @@ public class PrivilegesInterceptorImpl extends PrivilegesInterceptor { private static final String USER_TENANT = "__user__"; private static final String EMPTY_STRING = ""; private static final Map KIBANA_INDEX_SETTINGS = ImmutableMap.of( - IndexMetadata.SETTING_NUMBER_OF_SHARDS, 1, - IndexMetadata.SETTING_AUTO_EXPAND_REPLICAS, "0-1" + IndexMetadata.SETTING_NUMBER_OF_SHARDS, + 1, + IndexMetadata.SETTING_AUTO_EXPAND_REPLICAS, + "0-1" ); protected final Logger log = LogManager.getLogger(this.getClass()); - public PrivilegesInterceptorImpl(IndexNameExpressionResolver resolver, ClusterService clusterService, Client client, ThreadPool threadPool) { + public PrivilegesInterceptorImpl( + IndexNameExpressionResolver resolver, + ClusterService clusterService, + Client client, + ThreadPool threadPool + ) { super(resolver, clusterService, client, threadPool); } - private boolean isTenantAllowed(final ActionRequest request, final String action, final User user, final Map tenants, - final String requestedTenant) { + private boolean isTenantAllowed( + final ActionRequest request, + final String action, + final User user, + final Map tenants, + final String requestedTenant + ) { if (!tenants.keySet().contains(requestedTenant)) { log.warn("Tenant {} is not allowed for user {}", requestedTenant, user.getName()); @@ -94,23 +106,29 @@ private boolean isTenantAllowed(final ActionRequest request, final String action * */ @Override - public ReplaceResult replaceDashboardsIndex(final ActionRequest request, final String action, final User user, final DynamicConfigModel config, - final Resolved requestedResolved, final Map tenants) { + public ReplaceResult replaceDashboardsIndex( + final ActionRequest request, + final String action, + final User user, + final DynamicConfigModel config, + final Resolved requestedResolved, + final Map tenants + ) { - final boolean enabled = config.isDashboardsMultitenancyEnabled();//config.dynamic.kibana.multitenancy_enabled; + final boolean enabled = config.isDashboardsMultitenancyEnabled();// config.dynamic.kibana.multitenancy_enabled; if (!enabled) { return CONTINUE_EVALUATION_REPLACE_RESULT; } - //next two lines needs to be retrieved from configuration - final String dashboardsServerUsername = config.getDashboardsServerUsername();//config.dynamic.kibana.server_username; - final String dashboardsIndexName = config.getDashboardsIndexname();//config.dynamic.kibana.index; + // next two lines needs to be retrieved from configuration + final String dashboardsServerUsername = config.getDashboardsServerUsername();// config.dynamic.kibana.server_username; + final String dashboardsIndexName = config.getDashboardsIndexname();// config.dynamic.kibana.index; String requestedTenant = user.getRequestedTenant(); - if(USER_TENANT.equals(requestedTenant)) { + if (USER_TENANT.equals(requestedTenant)) { final boolean private_tenant_enabled = config.isDashboardsPrivateTenantEnabled(); - if(!private_tenant_enabled) { + if (!private_tenant_enabled) { return ACCESS_DENIED_REPLACE_RESULT; } } @@ -120,8 +138,10 @@ public ReplaceResult replaceDashboardsIndex(final ActionRequest request, final S log.debug("raw requestedTenant: '" + requestedTenant + "'"); } - //intercept when requests are not made by the kibana server and if the kibana index/alias (.kibana) is the only index/alias involved - final boolean dashboardsIndexOnly = !user.getName().equals(dashboardsServerUsername) && resolveToDashboardsIndexOrAlias(requestedResolved, dashboardsIndexName); + // intercept when requests are not made by the kibana server and if the kibana index/alias (.kibana) is the only index/alias + // involved + final boolean dashboardsIndexOnly = !user.getName().equals(dashboardsServerUsername) + && resolveToDashboardsIndexOrAlias(requestedResolved, dashboardsIndexName); final boolean isTraceEnabled = log.isTraceEnabled(); if (requestedTenant == null || requestedTenant.length() == 0) { if (isTraceEnabled) { @@ -140,21 +160,23 @@ public ReplaceResult replaceDashboardsIndex(final ActionRequest request, final S } if (isDebugEnabled && !user.getName().equals(dashboardsServerUsername)) { - //log statements only here + // log statements only here log.debug("requestedResolved: " + requestedResolved); } - //request not made by the kibana server and user index is the only index/alias involved + // request not made by the kibana server and user index is the only index/alias involved if (!user.getName().equals(dashboardsServerUsername) && !requestedResolved.isLocalAll()) { final Set indices = requestedResolved.getAllIndices(); final String tenantIndexName = toUserIndexName(dashboardsIndexName, requestedTenant); - if (indices.size() == 1 && indices.iterator().next().startsWith(tenantIndexName) && - isTenantAllowed(request, action, user, tenants, requestedTenant)) { - return ACCESS_GRANTED_REPLACE_RESULT; + if (indices.size() == 1 + && indices.iterator().next().startsWith(tenantIndexName) + && isTenantAllowed(request, action, user, tenants, requestedTenant)) { + return ACCESS_GRANTED_REPLACE_RESULT; } } - //intercept when requests are not made by the kibana server and if the kibana index/alias (.kibana) is the only index/alias involved + // intercept when requests are not made by the kibana server and if the kibana index/alias (.kibana) is the only index/alias + // involved if (dashboardsIndexOnly) { if (isDebugEnabled) { @@ -211,7 +233,8 @@ private CreateIndexRequestBuilder newCreateIndexRequestBuilderIfAbsent(final Str } String concreteName = getConcreteIndexName(name, indicesLookup); if (concreteName != null) { - return client.admin().indices() + return client.admin() + .indices() .prepareCreate(concreteName) .addAlias(new Alias(name)) .setSettings(KIBANA_INDEX_SETTINGS) @@ -220,7 +243,12 @@ private CreateIndexRequestBuilder newCreateIndexRequestBuilderIfAbsent(final Str return null; } - private CreateIndexRequestBuilder replaceIndex(final ActionRequest request, final String oldIndexName, final String newIndexName, final String action) { + private CreateIndexRequestBuilder replaceIndex( + final ActionRequest request, + final String oldIndexName, + final String newIndexName, + final String action + ) { boolean kibOk = false; CreateIndexRequestBuilder createIndexRequestBuilder = null; @@ -228,10 +256,10 @@ private CreateIndexRequestBuilder replaceIndex(final ActionRequest request, fina log.debug("{} index will be replaced with {} in this {} request", oldIndexName, newIndexName, request.getClass().getName()); } - //handle msearch and mget - //in case of GET change the .kibana index to the userskibanaindex - //in case of Search add the usersDashboardsindex - //if (request instanceof CompositeIndicesRequest) { + // handle msearch and mget + // in case of GET change the .kibana index to the userskibanaindex + // in case of Search add the usersDashboardsindex + // if (request instanceof CompositeIndicesRequest) { String[] newIndexNames = new String[] { newIndexName }; // CreateIndexRequest @@ -312,7 +340,7 @@ private CreateIndexRequestBuilder replaceIndex(final ActionRequest request, fina ((SingleShardRequest) request).index(newIndexName); kibOk = true; } else if (request instanceof RefreshRequest) { - ((RefreshRequest) request).indices(newIndexNames); //??? + ((RefreshRequest) request).indices(newIndexNames); // ??? kibOk = true; } else if (request instanceof ReplicationRequest) { ((ReplicationRequest) request).index(newIndexName); diff --git a/src/main/java/org/opensearch/security/configuration/Salt.java b/src/main/java/org/opensearch/security/configuration/Salt.java index cde4104fa2..3799fa846f 100644 --- a/src/main/java/org/opensearch/security/configuration/Salt.java +++ b/src/main/java/org/opensearch/security/configuration/Salt.java @@ -45,13 +45,19 @@ public Salt(final byte[] salt) { private Salt(final String saltAsString) { this.salt16 = new byte[SALT_SIZE]; if (saltAsString.equals(ConfigConstants.SECURITY_COMPLIANCE_SALT_DEFAULT)) { - log.warn("If you plan to use field masking pls configure compliance salt {} to be a random string of 16 chars length identical on all nodes", saltAsString); + log.warn( + "If you plan to use field masking pls configure compliance salt {} to be a random string of 16 chars length identical on all nodes", + saltAsString + ); } try { ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(saltAsString); byteBuffer.get(salt16); if (byteBuffer.remaining() > 0) { - log.warn("Provided compliance salt {} is greater than 16 bytes. Only the first 16 bytes are used for salting", saltAsString); + log.warn( + "Provided compliance salt {} is greater than 16 bytes. Only the first 16 bytes are used for salting", + saltAsString + ); } } catch (BufferUnderflowException e) { throw new OpenSearchException("Provided compliance salt " + saltAsString + " must at least contain 16 bytes", e); @@ -73,7 +79,10 @@ byte[] getSalt16() { * @return configuration */ public static Salt from(final Settings settings) { - final String saltAsString = settings.get(ConfigConstants.SECURITY_COMPLIANCE_SALT, ConfigConstants.SECURITY_COMPLIANCE_SALT_DEFAULT); + final String saltAsString = settings.get( + ConfigConstants.SECURITY_COMPLIANCE_SALT, + ConfigConstants.SECURITY_COMPLIANCE_SALT_DEFAULT + ); return new Salt(saltAsString); } } diff --git a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java index cfb2a1de61..2e58424c63 100644 --- a/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SecurityFlsDlsIndexSearcherWrapper.java @@ -39,11 +39,24 @@ public class SecurityFlsDlsIndexSearcherWrapper extends SecurityIndexSearcherWrapper { // TODO: the list is outdated. It is necessary to change how meta fields are handled in the near future. - // We may consider using MapperService.isMetadataField() instead of relying on the static set or - // (if it is too costly or does not meet requirements) use IndicesModule.getBuiltInMetadataFields() - // for OpenSearch version specific Set of meta fields - private static final Set metaFields = Sets.newHashSet("_source", "_version", "_field_names", - "_seq_no", "_primary_term", "_id", IgnoredFieldMapper.NAME, "_index", "_routing", "_size", "_timestamp", "_ttl", "_type"); + // We may consider using MapperService.isMetadataField() instead of relying on the static set or + // (if it is too costly or does not meet requirements) use IndicesModule.getBuiltInMetadataFields() + // for OpenSearch version specific Set of meta fields + private static final Set metaFields = Sets.newHashSet( + "_source", + "_version", + "_field_names", + "_seq_no", + "_primary_term", + "_id", + IgnoredFieldMapper.NAME, + "_index", + "_routing", + "_size", + "_timestamp", + "_ttl", + "_type" + ); private final ClusterService clusterService; private final IndexService indexService; private final AuditLog auditlog; @@ -51,9 +64,16 @@ public class SecurityFlsDlsIndexSearcherWrapper extends SecurityIndexSearcherWra private final DlsQueryParser dlsQueryParser; private final Salt salt; - public SecurityFlsDlsIndexSearcherWrapper(final IndexService indexService, final Settings settings, - final AdminDNs adminDNs, final ClusterService clusterService, final AuditLog auditlog, - final ComplianceIndexingOperationListener ciol, final PrivilegesEvaluator evaluator, final Salt salt) { + public SecurityFlsDlsIndexSearcherWrapper( + final IndexService indexService, + final Settings settings, + final AdminDNs adminDNs, + final ClusterService clusterService, + final AuditLog auditlog, + final ComplianceIndexingOperationListener ciol, + final PrivilegesEvaluator evaluator, + final Salt salt + ) { super(indexService, settings, adminDNs, evaluator); ciol.setIs(indexService); this.clusterService = clusterService; @@ -64,7 +84,7 @@ public SecurityFlsDlsIndexSearcherWrapper(final IndexService indexService, final if (allowNowinDlsQueries) { nowInMillis = () -> System.currentTimeMillis(); } else { - nowInMillis = () -> {throw new IllegalArgumentException("'now' is not allowed in DLS queries");}; + nowInMillis = () -> { throw new IllegalArgumentException("'now' is not allowed in DLS queries"); }; } log.debug("FLS/DLS {} enabled for index {}", this, indexService.index().getName()); this.salt = salt; @@ -80,14 +100,20 @@ protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdm Set maskedFields = null; Query dlsQuery = null; - if(!isAdmin) { - - final Map> allowedFlsFields = (Map>) HeaderHelper.deserializeSafeFromHeader(threadContext, - ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER); - final Map> queries = (Map>) HeaderHelper.deserializeSafeFromHeader(threadContext, - ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER); - final Map> maskedFieldsMap = (Map>) HeaderHelper.deserializeSafeFromHeader(threadContext, - ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER); + if (!isAdmin) { + + final Map> allowedFlsFields = (Map>) HeaderHelper.deserializeSafeFromHeader( + threadContext, + ConfigConstants.OPENDISTRO_SECURITY_FLS_FIELDS_HEADER + ); + final Map> queries = (Map>) HeaderHelper.deserializeSafeFromHeader( + threadContext, + ConfigConstants.OPENDISTRO_SECURITY_DLS_QUERY_HEADER + ); + final Map> maskedFieldsMap = (Map>) HeaderHelper.deserializeSafeFromHeader( + threadContext, + ConfigConstants.OPENDISTRO_SECURITY_MASKED_FIELD_HEADER + ); final String flsEval = SecurityUtils.evalMap(allowedFlsFields, index.getName()); final String dlsEval = SecurityUtils.evalMap(queries, index.getName()); @@ -114,7 +140,17 @@ protected DirectoryReader dlsFlsWrap(final DirectoryReader reader, boolean isAdm } } - return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader(reader, flsFields, dlsQuery, - indexService, threadContext, clusterService, auditlog, maskedFields, shardId, salt); + return new DlsFlsFilterLeafReader.DlsFlsDirectoryReader( + reader, + flsFields, + dlsQuery, + indexService, + threadContext, + clusterService, + auditlog, + maskedFields, + shardId, + salt + ); } } diff --git a/src/main/java/org/opensearch/security/configuration/SecurityIndexSearcherWrapper.java b/src/main/java/org/opensearch/security/configuration/SecurityIndexSearcherWrapper.java index a998b5f278..9c7d451fa3 100644 --- a/src/main/java/org/opensearch/security/configuration/SecurityIndexSearcherWrapper.java +++ b/src/main/java/org/opensearch/security/configuration/SecurityIndexSearcherWrapper.java @@ -47,7 +47,7 @@ import org.opensearch.security.support.WildcardMatcher; import org.opensearch.security.user.User; -public class SecurityIndexSearcherWrapper implements CheckedFunction { +public class SecurityIndexSearcherWrapper implements CheckedFunction { protected final Logger log = LogManager.getLogger(this.getClass()); protected final ThreadContext threadContext; @@ -63,18 +63,32 @@ public class SecurityIndexSearcherWrapper implements CheckedFunction existingConfiguration = load(getConfigName(), false); - - if (!isWriteable(channel, existingConfiguration, name)) { - return; - } - - boolean existed = existingConfiguration.exists(name); - existingConfiguration.remove(name); - - if (existed) { - AbstractApiAction.saveAndUpdateConfigs(this.securityIndexName, client, getConfigName(), existingConfiguration, new OnSucessActionListener(channel) { - - @Override - public void onResponse(IndexResponse response) { - successResponse(channel, "'" + name + "' deleted."); - } - }); - - } else { - notFound(channel, getResourceName() + " " + name + " not found."); - } - } - - protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { - - final String name = request.param("name"); - - if (name == null || name.length() == 0) { - badRequestResponse(channel, "No " + getResourceName() + " specified."); - return; - } - - final SecurityDynamicConfiguration existingConfiguration = load(getConfigName(), false); - - if (existingConfiguration.getSeqNo() < 0) { - forbidden(channel, "Security index need to be updated to support '" + getConfigName().toLCString() + "'. Use SecurityAdmin to populate."); - return; - } - - if (!isWriteable(channel, existingConfiguration, name)) { - return; - } - - if (isReadonlyFieldUpdated(existingConfiguration, content)) { - conflict(channel, "Attempted to update read-only property."); - return; - } - - if (log.isTraceEnabled() && content != null) { - log.trace(content.toString()); - } - - boolean existed = existingConfiguration.exists(name); - final Object newContent = DefaultObjectMapper.readTree(content, existingConfiguration.getImplementingClass()); - if (!hasPermissionsToCreate(existingConfiguration, newContent, getResourceName())) { - forbidden(channel, "No permissions"); - return; - } - existingConfiguration.putCObject(name, newContent); - - AbstractApiAction.saveAndUpdateConfigs(this.securityIndexName, client, getConfigName(), existingConfiguration, new OnSucessActionListener(channel) { - - @Override - public void onResponse(IndexResponse response) { - if (existed) { - successResponse(channel, "'" + name + "' updated."); - } else { - createdResponse(channel, "'" + name + "' created."); - } - - } - }); - - } - - protected void handlePost(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { - notImplemented(channel, Method.POST); - } - - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName) throws IOException { - return false; - } + protected abstract String getResourceName(); + + protected abstract CType getConfigName(); + + protected void handleApiRequest(final RestChannel channel, final RestRequest request, final Client client) throws IOException { + + try { + // validate additional settings, if any + AbstractConfigurationValidator validator = getValidator(request, request.content()); + if (!validator.validate()) { + request.params().clear(); + badRequestResponse(channel, validator); + return; + } + switch (request.method()) { + case DELETE: + handleDelete(channel, request, client, validator.getContentAsNode()); + break; + case POST: + handlePost(channel, request, client, validator.getContentAsNode()); + break; + case PUT: + handlePut(channel, request, client, validator.getContentAsNode()); + break; + case GET: + handleGet(channel, request, client, validator.getContentAsNode()); + break; + default: + throw new IllegalArgumentException(request.method() + " not supported"); + } + } catch (JsonMappingException jme) { + throw jme; + // TODO strip source + // if(jme.getLocation() == null || jme.getLocation().getSourceRef() == null) { + // throw jme; + // } else throw new JsonMappingException(null, jme.getMessage()); + } + } - protected void handleGet(final RestChannel channel, RestRequest request, Client client, final JsonNode content) - throws IOException{ + protected void handleDelete(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { + final String name = request.param("name"); + + if (name == null || name.length() == 0) { + badRequestResponse(channel, "No " + getResourceName() + " specified."); + return; + } + + final SecurityDynamicConfiguration existingConfiguration = load(getConfigName(), false); + + if (!isWriteable(channel, existingConfiguration, name)) { + return; + } + + boolean existed = existingConfiguration.exists(name); + existingConfiguration.remove(name); + + if (existed) { + AbstractApiAction.saveAndUpdateConfigs( + this.securityIndexName, + client, + getConfigName(), + existingConfiguration, + new OnSucessActionListener(channel) { + + @Override + public void onResponse(IndexResponse response) { + successResponse(channel, "'" + name + "' deleted."); + } + } + ); + + } else { + notFound(channel, getResourceName() + " " + name + " not found."); + } + } - final String resourcename = request.param("name"); + protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { + + final String name = request.param("name"); + + if (name == null || name.length() == 0) { + badRequestResponse(channel, "No " + getResourceName() + " specified."); + return; + } + + final SecurityDynamicConfiguration existingConfiguration = load(getConfigName(), false); + + if (existingConfiguration.getSeqNo() < 0) { + forbidden( + channel, + "Security index need to be updated to support '" + getConfigName().toLCString() + "'. Use SecurityAdmin to populate." + ); + return; + } + + if (!isWriteable(channel, existingConfiguration, name)) { + return; + } + + if (isReadonlyFieldUpdated(existingConfiguration, content)) { + conflict(channel, "Attempted to update read-only property."); + return; + } + + if (log.isTraceEnabled() && content != null) { + log.trace(content.toString()); + } + + boolean existed = existingConfiguration.exists(name); + final Object newContent = DefaultObjectMapper.readTree(content, existingConfiguration.getImplementingClass()); + if (!hasPermissionsToCreate(existingConfiguration, newContent, getResourceName())) { + forbidden(channel, "No permissions"); + return; + } + existingConfiguration.putCObject(name, newContent); + + AbstractApiAction.saveAndUpdateConfigs( + this.securityIndexName, + client, + getConfigName(), + existingConfiguration, + new OnSucessActionListener(channel) { + + @Override + public void onResponse(IndexResponse response) { + if (existed) { + successResponse(channel, "'" + name + "' updated."); + } else { + createdResponse(channel, "'" + name + "' created."); + } + + } + } + ); - final SecurityDynamicConfiguration configuration = load(getConfigName(), true); - filter(configuration); + } + protected void handlePost(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { + notImplemented(channel, Method.POST); + } - // no specific resource requested, return complete config - if (resourcename == null || resourcename.length() == 0) { + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) throws IOException { + return false; + } - successResponse(channel, configuration); - return; - } + protected void handleGet(final RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { - if (!configuration.exists(resourcename)) { - notFound(channel, "Resource '" + resourcename + "' not found."); - return; - } + final String resourcename = request.param("name"); - configuration.removeOthers(resourcename); - successResponse(channel, configuration); + final SecurityDynamicConfiguration configuration = load(getConfigName(), true); + filter(configuration); - return; - } + // no specific resource requested, return complete config + if (resourcename == null || resourcename.length() == 0) { + + successResponse(channel, configuration); + return; + } + + if (!configuration.exists(resourcename)) { + notFound(channel, "Resource '" + resourcename + "' not found."); + return; + } + + configuration.removeOthers(resourcename); + successResponse(channel, configuration); + + return; + } - protected final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { - SecurityDynamicConfiguration loaded = cl.getConfigurationsFromIndex(Collections.singleton(config), logComplianceEvent).get(config).deepClone(); - return DynamicConfigFactory.addStatics(loaded); - } + protected final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { + SecurityDynamicConfiguration loaded = cl.getConfigurationsFromIndex(Collections.singleton(config), logComplianceEvent) + .get(config) + .deepClone(); + return DynamicConfigFactory.addStatics(loaded); + } - protected final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent, boolean acceptInvalid) { - SecurityDynamicConfiguration loaded = cl.getConfigurationsFromIndex(Collections.singleton(config), logComplianceEvent, acceptInvalid).get(config).deepClone(); + protected final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent, boolean acceptInvalid) { + SecurityDynamicConfiguration loaded = cl.getConfigurationsFromIndex( + Collections.singleton(config), + logComplianceEvent, + acceptInvalid + ).get(config).deepClone(); return DynamicConfigFactory.addStatics(loaded); } - protected boolean ensureIndexExists() { - if (!cs.state().metadata().hasConcreteIndex(this.securityIndexName)) { - return false; - } - return true; - } - - protected void filter(SecurityDynamicConfiguration builder) { - if (!isSuperAdmin()){ - builder.removeHidden(); - } - builder.set_meta(null); - } - - protected boolean isReadonlyFieldUpdated(final JsonNode existingResource, final JsonNode targetResource) { - // Default is false. Override function for additional logic - return false; - } - - protected boolean isReadonlyFieldUpdated(final SecurityDynamicConfiguration configuration, final JsonNode targetResource) { - // Default is false. Override function for additional logic - return false; - } - - abstract class OnSucessActionListener implements ActionListener { - - private final RestChannel channel; - - public OnSucessActionListener(RestChannel channel) { - super(); - this.channel = channel; - } - - @Override - public final void onFailure(Exception e) { - if (ExceptionsHelper.unwrapCause(e) instanceof VersionConflictEngineException) { - conflict(channel, e.getMessage()); - } else { - internalErrorResponse(channel, "Error "+e.getMessage()); - } - } - - } - - public static void saveAndUpdateConfigs(final String indexName, final Client client, final CType cType, final SecurityDynamicConfiguration configuration, final ActionListener actionListener) { - final IndexRequest ir = new IndexRequest(indexName); - final String id = cType.toLCString(); - - configuration.removeStatic(); - - try { - client.index(ir.id(id) - .setRefreshPolicy(RefreshPolicy.IMMEDIATE) - .setIfSeqNo(configuration.getSeqNo()) - .setIfPrimaryTerm(configuration.getPrimaryTerm()) - .source(id, XContentHelper.toXContent(configuration, XContentType.JSON, false)), - new ConfigUpdatingActionListener<>(new String[]{id}, client, actionListener)); - } catch (IOException e) { - throw ExceptionsHelper.convertToOpenSearchException(e); - } - } - - protected static class ConfigUpdatingActionListener implements ActionListener { - private final String[] cTypes; - private final Client client; - private final ActionListener delegate; - - public ConfigUpdatingActionListener(String[] cTypes, Client client, ActionListener delegate) { - this.cTypes = Objects.requireNonNull(cTypes, "cTypes must not be null"); - this.client = Objects.requireNonNull(client, "client must not be null"); - this.delegate = Objects.requireNonNull(delegate, "delegate must not be null"); - } - - @Override - public void onResponse(Response response) { - - final ConfigUpdateRequest cur = new ConfigUpdateRequest(cTypes); - - client.execute(ConfigUpdateAction.INSTANCE, cur, new ActionListener() { - @Override - public void onResponse(final ConfigUpdateResponse ur) { - if(ur.hasFailures()) { - delegate.onFailure(ur.failures().get(0)); - return; - } - delegate.onResponse(response); - } - - @Override - public void onFailure(final Exception e) { - delegate.onFailure(e); - } - }); - - } - - @Override - public void onFailure(Exception e) { - delegate.onFailure(e); - } - - } - - @Override - protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - - // consume all parameters first so we can return a correct HTTP status, - // not 400 - consumeParameters(request); - - // check if .opendistro_security index has been initialized - if (!ensureIndexExists()) { - return channel -> internalErrorResponse(channel, ErrorType.SECURITY_NOT_INITIALIZED.getMessage()); - } - - // check if request is authorized - String authError = restApiPrivilegesEvaluator.checkAccessPermissions(request, getEndpoint()); - - final User user = (User) threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - final String userName = user == null ? null : user.getName(); - if (authError != null) { - log.error("No permission to access REST API: " + authError); - auditLog.logMissingPrivileges(authError, userName, request); - // for rest request - request.params().clear(); - return channel -> forbidden(channel, "No permission to access REST API: " + authError); - } else { - auditLog.logGrantedPrivileges(userName, request); - } - - final Object originalUser = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - final Object originalRemoteAddress = threadPool.getThreadContext() - .getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - final Object originalOrigin = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN); - - return channel -> threadPool.generic().submit(() -> { - try (StoredContext ignore = threadPool.getThreadContext().stashContext()) { - threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); - threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, originalUser); - threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, originalRemoteAddress); - threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, originalOrigin); - - handleApiRequest(channel, request, client); - } catch (Exception e) { - log.error("Error processing request {}", request, e); - try { - channel.sendResponse(new BytesRestResponse(channel, e)); - } catch (IOException ioe) { - throw ExceptionsHelper.convertToOpenSearchException(e); - } - } - }); - } - - protected boolean checkConfigUpdateResponse(final ConfigUpdateResponse response) { - - final int nodeCount = cs.state().getNodes().getNodes().size(); - final int expectedConfigCount = 1; - - boolean success = response.getNodes().size() == nodeCount; - if (!success) { - log.error( - "Expected " + nodeCount + " nodes to return response, but got only " + response.getNodes().size()); - } - - for (final String nodeId : response.getNodesMap().keySet()) { - final ConfigUpdateNodeResponse node = response.getNodesMap().get(nodeId); - final boolean successNode = node.getUpdatedConfigTypes() != null - && node.getUpdatedConfigTypes().length == expectedConfigCount; - - if (!successNode) { - log.error("Expected " + expectedConfigCount + " config types for node " + nodeId + " but got only " - + Arrays.toString(node.getUpdatedConfigTypes())); - } - - success = success && successNode; - } - - return success; - } - - protected static XContentBuilder convertToJson(RestChannel channel, ToXContent toxContent) { - try { - XContentBuilder builder = channel.newBuilder(); - toxContent.toXContent(builder, ToXContent.EMPTY_PARAMS); - return builder; - } catch (IOException e) { - throw ExceptionsHelper.convertToOpenSearchException(e); - } - } - - protected void response(RestChannel channel, RestStatus status, String message) { - try { - final XContentBuilder builder = channel.newBuilder(); - builder.startObject(); - builder.field("status", status.name()); - builder.field("message", message); - builder.endObject(); - channel.sendResponse(new BytesRestResponse(status, builder)); - } catch (IOException e) { - throw ExceptionsHelper.convertToOpenSearchException(e); - } - } - - protected void successResponse(RestChannel channel, SecurityDynamicConfiguration response) { - channel.sendResponse( - new BytesRestResponse(RestStatus.OK, convertToJson(channel, response))); - } - - protected void successResponse(RestChannel channel) { - try { - final XContentBuilder builder = channel.newBuilder(); - builder.startObject(); - builder.endObject(); - channel.sendResponse( - new BytesRestResponse(RestStatus.OK, builder)); - } catch (IOException e) { - internalErrorResponse(channel, "Unable to fetch license: " + e.getMessage()); - log.error("Cannot fetch convert license to XContent due to", e); - } - } - - protected void badRequestResponse(RestChannel channel, AbstractConfigurationValidator validator) { - channel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, validator.errorsAsXContent(channel))); - } - - protected void successResponse(RestChannel channel, String message) { - response(channel, RestStatus.OK, message); - } - - protected void createdResponse(RestChannel channel, String message) { - response(channel, RestStatus.CREATED, message); - } - - protected void badRequestResponse(RestChannel channel, String message) { - response(channel, RestStatus.BAD_REQUEST, message); - } - - protected void notFound(RestChannel channel, String message) { - response(channel, RestStatus.NOT_FOUND, message); - } - - protected void forbidden(RestChannel channel, String message) { - response(channel, RestStatus.FORBIDDEN, message); - } - - protected void internalErrorResponse(RestChannel channel, String message) { - response(channel, RestStatus.INTERNAL_SERVER_ERROR, message); - } - - protected void unprocessable(RestChannel channel, String message) { - response(channel, RestStatus.UNPROCESSABLE_ENTITY, message); - } - - protected void conflict(RestChannel channel, String message) { - response(channel, RestStatus.CONFLICT, message); - } - - protected void notImplemented(RestChannel channel, Method method) { - response(channel, RestStatus.NOT_IMPLEMENTED, - "Method " + method.name() + " not supported for this action."); - } - - protected final boolean isReserved(SecurityDynamicConfiguration configuration, String resourceName) { - if(isStatic(configuration, resourceName)) { //static is also always reserved - return true; - } - - final Object o = configuration.getCEntry(resourceName); - return o != null && o instanceof Hideable && ((Hideable) o).isReserved(); - } - - protected final boolean isHidden(SecurityDynamicConfiguration configuration, String resourceName) { - return configuration.isHidden(resourceName) && !isSuperAdmin(); - } - - protected final boolean isStatic(SecurityDynamicConfiguration configuration, String resourceName) { - final Object o = configuration.getCEntry(resourceName); - return o != null && o instanceof StaticDefinable && ((StaticDefinable) o).isStatic(); - } - - /** - * Consume all defined parameters for the request. Before we handle the - * request in subclasses where we actually need the parameter, some global - * checks are performed, e.g. check whether the .security_index index exists. Thus, the - * parameter(s) have not been consumed, and OpenSearch will always return a 400 with - * an internal error message. - * - * @param request - */ - protected void consumeParameters(final RestRequest request) { - request.param("name"); - } - - @Override - public String getName() { - return getClass().getSimpleName(); - } - - protected abstract Endpoint getEndpoint(); - - protected boolean isSuperAdmin() { - return restApiAdminPrivilegesEvaluator.isCurrentUserRestApiAdminFor(getEndpoint()); - } - - /** - * Resource is readonly if it is reserved and user is not super admin. - * @param existingConfiguration Configuration - * @param name - * @return True if resource readonly - */ - protected boolean isReadOnly(final SecurityDynamicConfiguration existingConfiguration, - String name) { - return isSuperAdmin() ? false: isReserved(existingConfiguration, name); - } - - /** - * Checks if it is valid to add role to opendistro_security_roles or rolesmapping. - * Role can be mapped to user if it exists. Only superadmin can add hidden or reserved roles. - * - * @param channel Rest Channel for response - * @param role Name of the role - * @return True if role can be mapped - */ - protected boolean isValidRolesMapping(final RestChannel channel, final String role) { - final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); - final SecurityDynamicConfiguration rolesMappingConfiguration = load(CType.ROLESMAPPING, false); - - if (!rolesConfiguration.exists(role)) { - notFound(channel, "Role '"+role+"' is not available for role-mapping."); - return false; - } - - if (isHidden(rolesConfiguration, role)) { - notFound(channel, "Role '" + role + "' is not available for role-mapping."); - return false; - } - - return isWriteable(channel, rolesMappingConfiguration, role); - } - - boolean isWriteable(final RestChannel channel, final SecurityDynamicConfiguration configuration, final String resourceName) { - if (isHidden(configuration, resourceName)) { - notFound(channel, "Resource '" + resourceName + "' is not available."); - return false; - } - - if (isReadOnly(configuration, resourceName)) { - forbidden(channel, "Resource '" + resourceName + "' is read-only."); - return false; - } - return true; - } + protected boolean ensureIndexExists() { + if (!cs.state().metadata().hasConcreteIndex(this.securityIndexName)) { + return false; + } + return true; + } + + protected void filter(SecurityDynamicConfiguration builder) { + if (!isSuperAdmin()) { + builder.removeHidden(); + } + builder.set_meta(null); + } + + protected boolean isReadonlyFieldUpdated(final JsonNode existingResource, final JsonNode targetResource) { + // Default is false. Override function for additional logic + return false; + } + + protected boolean isReadonlyFieldUpdated(final SecurityDynamicConfiguration configuration, final JsonNode targetResource) { + // Default is false. Override function for additional logic + return false; + } + + abstract class OnSucessActionListener implements ActionListener { + + private final RestChannel channel; + + public OnSucessActionListener(RestChannel channel) { + super(); + this.channel = channel; + } + + @Override + public final void onFailure(Exception e) { + if (ExceptionsHelper.unwrapCause(e) instanceof VersionConflictEngineException) { + conflict(channel, e.getMessage()); + } else { + internalErrorResponse(channel, "Error " + e.getMessage()); + } + } + + } + + public static void saveAndUpdateConfigs( + final String indexName, + final Client client, + final CType cType, + final SecurityDynamicConfiguration configuration, + final ActionListener actionListener + ) { + final IndexRequest ir = new IndexRequest(indexName); + final String id = cType.toLCString(); + + configuration.removeStatic(); + + try { + client.index( + ir.id(id) + .setRefreshPolicy(RefreshPolicy.IMMEDIATE) + .setIfSeqNo(configuration.getSeqNo()) + .setIfPrimaryTerm(configuration.getPrimaryTerm()) + .source(id, XContentHelper.toXContent(configuration, XContentType.JSON, false)), + new ConfigUpdatingActionListener<>(new String[] { id }, client, actionListener) + ); + } catch (IOException e) { + throw ExceptionsHelper.convertToOpenSearchException(e); + } + } + + protected static class ConfigUpdatingActionListener implements ActionListener { + private final String[] cTypes; + private final Client client; + private final ActionListener delegate; + + public ConfigUpdatingActionListener(String[] cTypes, Client client, ActionListener delegate) { + this.cTypes = Objects.requireNonNull(cTypes, "cTypes must not be null"); + this.client = Objects.requireNonNull(client, "client must not be null"); + this.delegate = Objects.requireNonNull(delegate, "delegate must not be null"); + } + + @Override + public void onResponse(Response response) { + + final ConfigUpdateRequest cur = new ConfigUpdateRequest(cTypes); + + client.execute(ConfigUpdateAction.INSTANCE, cur, new ActionListener() { + @Override + public void onResponse(final ConfigUpdateResponse ur) { + if (ur.hasFailures()) { + delegate.onFailure(ur.failures().get(0)); + return; + } + delegate.onResponse(response); + } + + @Override + public void onFailure(final Exception e) { + delegate.onFailure(e); + } + }); + + } + + @Override + public void onFailure(Exception e) { + delegate.onFailure(e); + } + + } + + @Override + protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + + // consume all parameters first so we can return a correct HTTP status, + // not 400 + consumeParameters(request); + + // check if .opendistro_security index has been initialized + if (!ensureIndexExists()) { + return channel -> internalErrorResponse(channel, ErrorType.SECURITY_NOT_INITIALIZED.getMessage()); + } + + // check if request is authorized + String authError = restApiPrivilegesEvaluator.checkAccessPermissions(request, getEndpoint()); + + final User user = (User) threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final String userName = user == null ? null : user.getName(); + if (authError != null) { + log.error("No permission to access REST API: " + authError); + auditLog.logMissingPrivileges(authError, userName, request); + // for rest request + request.params().clear(); + return channel -> forbidden(channel, "No permission to access REST API: " + authError); + } else { + auditLog.logGrantedPrivileges(userName, request); + } + + final Object originalUser = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final Object originalRemoteAddress = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + final Object originalOrigin = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN); + + return channel -> threadPool.generic().submit(() -> { + try (StoredContext ignore = threadPool.getThreadContext().stashContext()) { + threadPool.getThreadContext().putHeader(ConfigConstants.OPENDISTRO_SECURITY_CONF_REQUEST_HEADER, "true"); + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, originalUser); + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS, originalRemoteAddress); + threadPool.getThreadContext().putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, originalOrigin); + + handleApiRequest(channel, request, client); + } catch (Exception e) { + log.error("Error processing request {}", request, e); + try { + channel.sendResponse(new BytesRestResponse(channel, e)); + } catch (IOException ioe) { + throw ExceptionsHelper.convertToOpenSearchException(e); + } + } + }); + } + + protected boolean checkConfigUpdateResponse(final ConfigUpdateResponse response) { + + final int nodeCount = cs.state().getNodes().getNodes().size(); + final int expectedConfigCount = 1; + + boolean success = response.getNodes().size() == nodeCount; + if (!success) { + log.error("Expected " + nodeCount + " nodes to return response, but got only " + response.getNodes().size()); + } + + for (final String nodeId : response.getNodesMap().keySet()) { + final ConfigUpdateNodeResponse node = response.getNodesMap().get(nodeId); + final boolean successNode = node.getUpdatedConfigTypes() != null && node.getUpdatedConfigTypes().length == expectedConfigCount; + + if (!successNode) { + log.error( + "Expected " + + expectedConfigCount + + " config types for node " + + nodeId + + " but got only " + + Arrays.toString(node.getUpdatedConfigTypes()) + ); + } + + success = success && successNode; + } + + return success; + } + + protected static XContentBuilder convertToJson(RestChannel channel, ToXContent toxContent) { + try { + XContentBuilder builder = channel.newBuilder(); + toxContent.toXContent(builder, ToXContent.EMPTY_PARAMS); + return builder; + } catch (IOException e) { + throw ExceptionsHelper.convertToOpenSearchException(e); + } + } + + protected void response(RestChannel channel, RestStatus status, String message) { + try { + final XContentBuilder builder = channel.newBuilder(); + builder.startObject(); + builder.field("status", status.name()); + builder.field("message", message); + builder.endObject(); + channel.sendResponse(new BytesRestResponse(status, builder)); + } catch (IOException e) { + throw ExceptionsHelper.convertToOpenSearchException(e); + } + } + + protected void successResponse(RestChannel channel, SecurityDynamicConfiguration response) { + channel.sendResponse(new BytesRestResponse(RestStatus.OK, convertToJson(channel, response))); + } + + protected void successResponse(RestChannel channel) { + try { + final XContentBuilder builder = channel.newBuilder(); + builder.startObject(); + builder.endObject(); + channel.sendResponse(new BytesRestResponse(RestStatus.OK, builder)); + } catch (IOException e) { + internalErrorResponse(channel, "Unable to fetch license: " + e.getMessage()); + log.error("Cannot fetch convert license to XContent due to", e); + } + } + + protected void badRequestResponse(RestChannel channel, AbstractConfigurationValidator validator) { + channel.sendResponse(new BytesRestResponse(RestStatus.BAD_REQUEST, validator.errorsAsXContent(channel))); + } + + protected void successResponse(RestChannel channel, String message) { + response(channel, RestStatus.OK, message); + } + + protected void createdResponse(RestChannel channel, String message) { + response(channel, RestStatus.CREATED, message); + } + + protected void badRequestResponse(RestChannel channel, String message) { + response(channel, RestStatus.BAD_REQUEST, message); + } + + protected void notFound(RestChannel channel, String message) { + response(channel, RestStatus.NOT_FOUND, message); + } + + protected void forbidden(RestChannel channel, String message) { + response(channel, RestStatus.FORBIDDEN, message); + } + + protected void internalErrorResponse(RestChannel channel, String message) { + response(channel, RestStatus.INTERNAL_SERVER_ERROR, message); + } + + protected void unprocessable(RestChannel channel, String message) { + response(channel, RestStatus.UNPROCESSABLE_ENTITY, message); + } + + protected void conflict(RestChannel channel, String message) { + response(channel, RestStatus.CONFLICT, message); + } + + protected void notImplemented(RestChannel channel, Method method) { + response(channel, RestStatus.NOT_IMPLEMENTED, "Method " + method.name() + " not supported for this action."); + } + + protected final boolean isReserved(SecurityDynamicConfiguration configuration, String resourceName) { + return configuration.isStatic(resourceName) || configuration.isReserved(resourceName); + } + + protected final boolean isHidden(SecurityDynamicConfiguration configuration, String resourceName) { + return configuration.isHidden(resourceName) && !isSuperAdmin(); + } + + /** + * Consume all defined parameters for the request. Before we handle the + * request in subclasses where we actually need the parameter, some global + * checks are performed, e.g. check whether the .security_index index exists. Thus, the + * parameter(s) have not been consumed, and OpenSearch will always return a 400 with + * an internal error message. + * + * @param request + */ + protected void consumeParameters(final RestRequest request) { + request.param("name"); + } + + @Override + public String getName() { + return getClass().getSimpleName(); + } + + protected abstract Endpoint getEndpoint(); + + protected boolean isSuperAdmin() { + return restApiAdminPrivilegesEvaluator.isCurrentUserRestApiAdminFor(getEndpoint()); + } + + /** + * Resource is readonly if it is reserved and user is not super admin. + * @param existingConfiguration Configuration + * @param name + * @return True if resource readonly + */ + protected boolean isReadOnly(final SecurityDynamicConfiguration existingConfiguration, String name) { + return !isSuperAdmin() && isReserved(existingConfiguration, name); + } + + /** + * Checks if it is valid to add role to opendistro_security_roles or rolesmapping. + * Role can be mapped to user if it exists. Only superadmin can add hidden or reserved roles. + * + * @param channel Rest Channel for response + * @param role Name of the role + * @return True if role can be mapped + */ + protected boolean isValidRolesMapping(final RestChannel channel, final String role) { + final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); + final SecurityDynamicConfiguration rolesMappingConfiguration = load(CType.ROLESMAPPING, false); + + if (!rolesConfiguration.exists(role)) { + notFound(channel, "Role '" + role + "' is not available for role-mapping."); + return false; + } + + if (isHidden(rolesConfiguration, role)) { + notFound(channel, "Role '" + role + "' is not available for role-mapping."); + return false; + } + + return isWriteable(channel, rolesMappingConfiguration, role); + } + + boolean isWriteable(final RestChannel channel, final SecurityDynamicConfiguration configuration, final String resourceName) { + if (isHidden(configuration, resourceName)) { + notFound(channel, "Resource '" + resourceName + "' is not available."); + return false; + } + + if (isReadOnly(configuration, resourceName)) { + forbidden(channel, "Resource '" + resourceName + "' is read-only."); + return false; + } + return true; + } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java index e5121f939d..68446366bc 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AccountApiAction.java @@ -59,34 +59,37 @@ */ public class AccountApiAction extends AbstractApiAction { private static final String RESOURCE_NAME = "account"; - private static final List routes = addRoutesPrefix(ImmutableList.of( - new Route(Method.GET, "/account"), - new Route(Method.PUT, "/account") - )); + private static final List routes = addRoutesPrefix( + ImmutableList.of(new Route(Method.GET, "/account"), new Route(Method.PUT, "/account")) + ); private final PrivilegesEvaluator privilegesEvaluator; private final ThreadContext threadContext; - public AccountApiAction(Settings settings, - Path configPath, - RestController controller, - Client client, - AdminDNs adminDNs, - ConfigurationRepository cl, - ClusterService cs, - PrincipalExtractor principalExtractor, - PrivilegesEvaluator privilegesEvaluator, - ThreadPool threadPool, - AuditLog auditLog) { + public AccountApiAction( + Settings settings, + Path configPath, + RestController controller, + Client client, + AdminDNs adminDNs, + ConfigurationRepository cl, + ClusterService cs, + PrincipalExtractor principalExtractor, + PrivilegesEvaluator privilegesEvaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, privilegesEvaluator, threadPool, auditLog); this.privilegesEvaluator = privilegesEvaluator; this.threadContext = threadPool.getThreadContext(); } @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName) { + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) { return true; } @@ -138,14 +141,14 @@ protected void handleGet(RestChannel channel, RestRequest request, Client client final SecurityDynamicConfiguration configuration = load(getConfigName(), false); builder.field("user_name", user.getName()) - .field("is_reserved", isReserved(configuration, user.getName())) - .field("is_hidden", configuration.isHidden(user.getName())) - .field("is_internal_user", configuration.exists(user.getName())) - .field("user_requested_tenant", user.getRequestedTenant()) - .field("backend_roles", user.getRoles()) - .field("custom_attribute_names", user.getCustomAttributesMap().keySet()) - .field("tenants", privilegesEvaluator.mapTenants(user, securityRoles)) - .field("roles", securityRoles); + .field("is_reserved", isReserved(configuration, user.getName())) + .field("is_hidden", configuration.isHidden(user.getName())) + .field("is_internal_user", configuration.exists(user.getName())) + .field("user_requested_tenant", user.getRequestedTenant()) + .field("backend_roles", user.getRoles()) + .field("custom_attribute_names", user.getCustomAttributesMap().keySet()) + .field("tenants", privilegesEvaluator.mapTenants(user, securityRoles)) + .field("roles", securityRoles); } builder.endObject(); @@ -153,9 +156,7 @@ protected void handleGet(RestChannel channel, RestRequest request, Client client } catch (final Exception exception) { log.error(exception.toString()); - builder.startObject() - .field("error", exception.toString()) - .endObject(); + builder.startObject().field("error", exception.toString()).endObject(); response = new BytesRestResponse(RestStatus.INTERNAL_SERVER_ERROR, builder); } @@ -185,7 +186,8 @@ protected void handleGet(RestChannel channel, RestRequest request, Client client * @throws IOException */ @Override - protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { + protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); final String username = user.getName(); final SecurityDynamicConfiguration internalUser = load(CType.INTERNALUSERS, false); @@ -224,12 +226,18 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C internalUserEntry.setHash(hash); - AccountApiAction.saveAndUpdateConfigs(this.securityIndexName, client, CType.INTERNALUSERS, internalUser, new OnSucessActionListener(channel) { - @Override - public void onResponse(IndexResponse response) { - successResponse(channel, "'" + username + "' updated."); + AccountApiAction.saveAndUpdateConfigs( + this.securityIndexName, + client, + CType.INTERNALUSERS, + internalUser, + new OnSucessActionListener(channel) { + @Override + public void onResponse(IndexResponse response) { + successResponse(channel, "'" + username + "' updated."); + } } - }); + ); } @Override diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java index 23a3a451b9..94af3ad3af 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ActionGroupsApiAction.java @@ -44,109 +44,123 @@ public class ActionGroupsApiAction extends PatchableResourceApiAction { - private static final List routes = addRoutesPrefix(ImmutableList.of( - // legacy mapping for backwards compatibility - // TODO: remove in next version - new Route(Method.GET, "/actiongroup/{name}"), - new Route(Method.GET, "/actiongroup/"), - new Route(Method.DELETE, "/actiongroup/{name}"), - new Route(Method.PUT, "/actiongroup/{name}"), - - // corrected mapping, introduced in OpenSearch Security - new Route(Method.GET, "/actiongroups/{name}"), - new Route(Method.GET, "/actiongroups/"), - new Route(Method.DELETE, "/actiongroups/{name}"), - new Route(Method.PUT, "/actiongroups/{name}"), - new Route(Method.PATCH, "/actiongroups/"), - new Route(Method.PATCH, "/actiongroups/{name}") - - )); - - @Override - protected Endpoint getEndpoint() { - return Endpoint.ACTIONGROUPS; - } - - @Inject - public ActionGroupsApiAction(final Settings settings, final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDNs, final ConfigurationRepository cl, final ClusterService cs, - final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); - } - - @Override - public List routes() { - return routes; - } - - @Override - protected AbstractConfigurationValidator getValidator(final RestRequest request, BytesReference ref, Object... param) { - return new ActionGroupValidator(request, isSuperAdmin(), ref, this.settings, param); - } - - @Override - protected CType getConfigName() { - return CType.ACTIONGROUPS; - } - - @Override + private static final List routes = addRoutesPrefix( + ImmutableList.of( + // legacy mapping for backwards compatibility + // TODO: remove in next version + new Route(Method.GET, "/actiongroup/{name}"), + new Route(Method.GET, "/actiongroup/"), + new Route(Method.DELETE, "/actiongroup/{name}"), + new Route(Method.PUT, "/actiongroup/{name}"), + + // corrected mapping, introduced in OpenSearch Security + new Route(Method.GET, "/actiongroups/{name}"), + new Route(Method.GET, "/actiongroups/"), + new Route(Method.DELETE, "/actiongroups/{name}"), + new Route(Method.PUT, "/actiongroups/{name}"), + new Route(Method.PATCH, "/actiongroups/"), + new Route(Method.PATCH, "/actiongroups/{name}") + + ) + ); + + @Override + protected Endpoint getEndpoint() { + return Endpoint.ACTIONGROUPS; + } + + @Inject + public ActionGroupsApiAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { + super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + } + + @Override + public List routes() { + return routes; + } + + @Override + protected AbstractConfigurationValidator getValidator(final RestRequest request, BytesReference ref, Object... param) { + return new ActionGroupValidator(request, isSuperAdmin(), ref, this.settings, param); + } + + @Override + protected CType getConfigName() { + return CType.ACTIONGROUPS; + } + + @Override protected String getResourceName() { return "actiongroup"; - } - - @Override - protected void consumeParameters(final RestRequest request) { - request.param("name"); - } - - @Override - protected void handlePut(RestChannel channel, RestRequest request, Client client, JsonNode content) throws IOException { - final String name = request.param("name"); - - if (name == null || name.length() == 0) { - badRequestResponse(channel, "No " + getResourceName() + " specified."); - return; - } - - // Prevent the case where action group and role share a same name. - SecurityDynamicConfiguration existingRolesConfig = load(CType.ROLES, false); - Set existingRoles = existingRolesConfig.getCEntries().keySet(); - if (existingRoles.contains(name)) { - badRequestResponse(channel, name + " is an existing role. A action group cannot be named with an existing role name."); - return; - } - - // Prevent the case where action group references to itself in the allowed_actions. - final SecurityDynamicConfiguration existingActionGroupsConfig = load(getConfigName(), false); - final Object actionGroup = DefaultObjectMapper.readTree(content, existingActionGroupsConfig.getImplementingClass()); - existingActionGroupsConfig.putCObject(name, actionGroup); - if (hasActionGroupSelfReference(existingActionGroupsConfig, name)) { - badRequestResponse(channel, name + " cannot be an allowed_action of itself"); - return; - } - // prevent creation of groups for REST admin api - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(actionGroup)) { - forbidden(channel, "Not allowed"); - return; - } - super.handlePut(channel, request, client, content); - } - - @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfiguration, - final Object content, - final String resourceName) throws IOException { - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(content)) { - return false; - } - return true; - } - - @Override - protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(existingConfiguration.getCEntry(name))) { - return true; - } - return super.isReadOnly(existingConfiguration, name); - } + } + + @Override + protected void consumeParameters(final RestRequest request) { + request.param("name"); + } + + @Override + protected void handlePut(RestChannel channel, RestRequest request, Client client, JsonNode content) throws IOException { + final String name = request.param("name"); + + if (name == null || name.length() == 0) { + badRequestResponse(channel, "No " + getResourceName() + " specified."); + return; + } + + // Prevent the case where action group and role share a same name. + SecurityDynamicConfiguration existingRolesConfig = load(CType.ROLES, false); + Set existingRoles = existingRolesConfig.getCEntries().keySet(); + if (existingRoles.contains(name)) { + badRequestResponse(channel, name + " is an existing role. A action group cannot be named with an existing role name."); + return; + } + + // Prevent the case where action group references to itself in the allowed_actions. + final SecurityDynamicConfiguration existingActionGroupsConfig = load(getConfigName(), false); + final Object actionGroup = DefaultObjectMapper.readTree(content, existingActionGroupsConfig.getImplementingClass()); + existingActionGroupsConfig.putCObject(name, actionGroup); + if (hasActionGroupSelfReference(existingActionGroupsConfig, name)) { + badRequestResponse(channel, name + " cannot be an allowed_action of itself"); + return; + } + // prevent creation of groups for REST admin api + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(actionGroup)) { + forbidden(channel, "Not allowed"); + return; + } + super.handlePut(channel, request, client, content); + } + + @Override + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfiguration, + final Object content, + final String resourceName + ) throws IOException { + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(content)) { + return false; + } + return true; + } + + @Override + protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(existingConfiguration.getCEntry(name))) { + return true; + } + return super.isReadOnly(existingConfiguration, name); + } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java index cefaeb5c6c..0c5b2775aa 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AllowlistApiAction.java @@ -9,7 +9,6 @@ * GitHub history for details. */ - package org.opensearch.security.dlic.rest.api; import java.io.IOException; @@ -84,24 +83,36 @@ */ public class AllowlistApiAction extends PatchableResourceApiAction { private static final List routes = ImmutableList.of( - new Route(RestRequest.Method.GET, "/_plugins/_security/api/allowlist"), - new Route(RestRequest.Method.PUT, "/_plugins/_security/api/allowlist"), - new Route(RestRequest.Method.PATCH, "/_plugins/_security/api/allowlist") + new Route(RestRequest.Method.GET, "/_plugins/_security/api/allowlist"), + new Route(RestRequest.Method.PUT, "/_plugins/_security/api/allowlist"), + new Route(RestRequest.Method.PATCH, "/_plugins/_security/api/allowlist") ); private static final String name = "config"; @Inject - public AllowlistApiAction(final Settings settings, final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDNs, final ConfigurationRepository cl, final ClusterService cs, - final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { + public AllowlistApiAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); } @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName) { + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) { return true; } @@ -115,9 +126,7 @@ protected void handleApiRequest(final RestChannel channel, final RestRequest req } @Override - protected void handleGet(final RestChannel channel, RestRequest request, Client client, final JsonNode content) - throws IOException { - + protected void handleGet(final RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { final SecurityDynamicConfiguration configuration = load(getConfigName(), true); filter(configuration); @@ -125,36 +134,46 @@ protected void handleGet(final RestChannel channel, RestRequest request, Client } @Override - protected void handleDelete(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { + protected void handleDelete(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { notImplemented(channel, RestRequest.Method.DELETE); } @Override - protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { + protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { final SecurityDynamicConfiguration existingConfiguration = load(getConfigName(), false); if (existingConfiguration.getSeqNo() < 0) { - forbidden(channel, "Security index need to be updated to support '" + getConfigName().toLCString() + "'. Use SecurityAdmin to populate."); + forbidden( + channel, + "Security index need to be updated to support '" + getConfigName().toLCString() + "'. Use SecurityAdmin to populate." + ); return; } boolean existed = existingConfiguration.exists(name); existingConfiguration.putCObject(name, DefaultObjectMapper.readTree(content, existingConfiguration.getImplementingClass())); - saveAndUpdateConfigs(this.securityIndexName,client, getConfigName(), existingConfiguration, new OnSucessActionListener(channel) { - - @Override - public void onResponse(IndexResponse response) { - if (existed) { - successResponse(channel, "'" + name + "' updated."); - } else { - createdResponse(channel, "'" + name + "' created."); + saveAndUpdateConfigs( + this.securityIndexName, + client, + getConfigName(), + existingConfiguration, + new OnSucessActionListener(channel) { + + @Override + public void onResponse(IndexResponse response) { + if (existed) { + successResponse(channel, "'" + name + "' updated."); + } else { + createdResponse(channel, "'" + name + "' created."); + } } } - }); + ); } - @Override public List routes() { return routes; diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java index e19f04d437..a61f66c6e3 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AuditApiAction.java @@ -124,11 +124,13 @@ * [{"op": "replace", "path": "/config/compliance/internal_config", "value": "true"}] */ public class AuditApiAction extends PatchableResourceApiAction { - private static final List routes = addRoutesPrefix(ImmutableList.of( + private static final List routes = addRoutesPrefix( + ImmutableList.of( new Route(RestRequest.Method.GET, "/audit/"), new Route(RestRequest.Method.PUT, "/audit/{name}"), new Route(RestRequest.Method.PATCH, "/audit/") - )); + ) + ); private static final String RESOURCE_NAME = "config"; @VisibleForTesting @@ -139,24 +141,28 @@ public class AuditApiAction extends PatchableResourceApiAction { private final PrivilegesEvaluator privilegesEvaluator; private final ThreadContext threadContext; - public AuditApiAction(final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator privilegesEvaluator, - final ThreadPool threadPool, - final AuditLog auditLog) { + public AuditApiAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator privilegesEvaluator, + final ThreadPool threadPool, + final AuditLog auditLog + ) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, privilegesEvaluator, threadPool, auditLog); this.privilegesEvaluator = privilegesEvaluator; this.threadContext = threadPool.getThreadContext(); try { - this.readonlyFields = DefaultObjectMapper.YAML_MAPPER - .readValue(this.getClass().getResourceAsStream(STATIC_RESOURCE), new TypeReference>>() {}) - .get(READONLY_FIELD); + this.readonlyFields = DefaultObjectMapper.YAML_MAPPER.readValue( + this.getClass().getResourceAsStream(STATIC_RESOURCE), + new TypeReference>>() { + } + ).get(READONLY_FIELD); if (!AuditConfig.FIELD_PATHS.containsAll(this.readonlyFields)) { throw new StaticResourceException("Invalid read-only field paths provided in static resource file " + STATIC_RESOURCE); } @@ -166,9 +172,11 @@ public AuditApiAction(final Settings settings, } @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName) { + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) { return true; } @@ -189,7 +197,8 @@ protected void handleApiRequest(final RestChannel channel, final RestRequest req } @Override - protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { + protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { if (!RESOURCE_NAME.equals(request.param("name"))) { badRequestResponse(channel, "name must be config"); return; @@ -245,9 +254,7 @@ protected CType getConfigName() { @Override protected boolean isReadonlyFieldUpdated(final JsonNode existingResource, final JsonNode targetResource) { if (!isSuperAdmin()) { - return readonlyFields - .stream() - .anyMatch(path -> !existingResource.at(path).equals(targetResource.at(path))); + return readonlyFields.stream().anyMatch(path -> !existingResource.at(path).equals(targetResource.at(path))); } return false; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/AuthTokenProcessorAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/AuthTokenProcessorAction.java index 497efcdf76..fa6d967624 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/AuthTokenProcessorAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/AuthTokenProcessorAction.java @@ -41,69 +41,77 @@ import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class AuthTokenProcessorAction extends AbstractApiAction { - private static final List routes = addRoutesPrefix(Collections.singletonList( - new Route(Method.POST, "/authtoken") - )); - - @Inject - public AuthTokenProcessorAction(final Settings settings, final Path configPath, final RestController controller, - final Client client, final AdminDNs adminDNs, final ConfigurationRepository cl, - final ClusterService cs, final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, - ThreadPool threadPool, AuditLog auditLog) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, - auditLog); - } - - @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName) { - return true; - } - - @Override - public List routes() { - return routes; - } - - @Override - protected void handlePost(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { - - // Just do nothing here. Eligible authenticators will intercept calls and - // provide own responses. - successResponse(channel,""); - } - - @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new NoOpValidator(request, ref, this.settings, param); - } - - @Override - protected String getResourceName() { - return "authtoken"; - } - - @Override + private static final List routes = addRoutesPrefix(Collections.singletonList(new Route(Method.POST, "/authtoken"))); + + @Inject + public AuthTokenProcessorAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { + super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + } + + @Override + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) { + return true; + } + + @Override + public List routes() { + return routes; + } + + @Override + protected void handlePost(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { + + // Just do nothing here. Eligible authenticators will intercept calls and + // provide own responses. + successResponse(channel, ""); + } + + @Override + protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { + return new NoOpValidator(request, ref, this.settings, param); + } + + @Override + protected String getResourceName() { + return "authtoken"; + } + + @Override protected CType getConfigName() { - return null; - } + return null; + } - @Override - protected Endpoint getEndpoint() { - return Endpoint.AUTHTOKEN; - } + @Override + protected Endpoint getEndpoint() { + return Endpoint.AUTHTOKEN; + } + public static class Response { + private String authorization; - public static class Response { - private String authorization; + public String getAuthorization() { + return authorization; + } - public String getAuthorization() { - return authorization; - } - - public void setAuthorization(String authorization) { - this.authorization = authorization; - } - } + public void setAuthorization(String authorization) { + this.authorization = authorization; + } + } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java index 406b81679c..2f8a60aa65 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/FlushCacheApiAction.java @@ -45,103 +45,118 @@ import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class FlushCacheApiAction extends AbstractApiAction { - private static final List routes = addRoutesPrefix(ImmutableList.of( - new Route(Method.DELETE, "/cache"), - new Route(Method.GET, "/cache"), - new Route(Method.PUT, "/cache"), - new Route(Method.POST, "/cache") - )); - - @Inject - public FlushCacheApiAction(final Settings settings, final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDNs, final ConfigurationRepository cl, final ClusterService cs, - final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); - } - - @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName) { - return true; - } - - @Override - public List routes() { - return routes; - } - - @Override - protected Endpoint getEndpoint() { - return Endpoint.CACHE; - } - - @Override - protected void handleDelete(RestChannel channel, - RestRequest request, Client client, final JsonNode content) throws IOException - { - - client.execute( - ConfigUpdateAction.INSTANCE, - new ConfigUpdateRequest(CType.lcStringValues().toArray(new String[0])), - new ActionListener() { - - @Override - public void onResponse(ConfigUpdateResponse ur) { - if(ur.hasFailures()) { - log.error("Cannot flush cache due to", ur.failures().get(0)); - internalErrorResponse(channel, "Cannot flush cache due to "+ ur.failures().get(0).getMessage()+"."); - return; - } - successResponse(channel, "Cache flushed successfully."); - if (log.isDebugEnabled()) { - log.debug("cache flushed successfully"); - } - } - - @Override - public void onFailure(Exception e) { - log.error("Cannot flush cache due to", e); - internalErrorResponse(channel, "Cannot flush cache due to "+ e.getMessage()+"."); - } - - } - ); - } - - @Override - protected void handlePost(RestChannel channel, final RestRequest request, final Client client, final JsonNode content)throws IOException { - notImplemented(channel, Method.POST); - } - - @Override - protected void handleGet(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException{ - notImplemented(channel, Method.GET); - } - - @Override - protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException{ - notImplemented(channel, Method.PUT); - } - - @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new NoOpValidator(request, ref, this.settings, param); - } - - @Override - protected String getResourceName() { - // not needed - return null; - } - - @Override + private static final List routes = addRoutesPrefix( + ImmutableList.of( + new Route(Method.DELETE, "/cache"), + new Route(Method.GET, "/cache"), + new Route(Method.PUT, "/cache"), + new Route(Method.POST, "/cache") + ) + ); + + @Inject + public FlushCacheApiAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { + super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + } + + @Override + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) { + return true; + } + + @Override + public List routes() { + return routes; + } + + @Override + protected Endpoint getEndpoint() { + return Endpoint.CACHE; + } + + @Override + protected void handleDelete(RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { + + client.execute( + ConfigUpdateAction.INSTANCE, + new ConfigUpdateRequest(CType.lcStringValues().toArray(new String[0])), + new ActionListener() { + + @Override + public void onResponse(ConfigUpdateResponse ur) { + if (ur.hasFailures()) { + log.error("Cannot flush cache due to", ur.failures().get(0)); + internalErrorResponse(channel, "Cannot flush cache due to " + ur.failures().get(0).getMessage() + "."); + return; + } + successResponse(channel, "Cache flushed successfully."); + if (log.isDebugEnabled()) { + log.debug("cache flushed successfully"); + } + } + + @Override + public void onFailure(Exception e) { + log.error("Cannot flush cache due to", e); + internalErrorResponse(channel, "Cannot flush cache due to " + e.getMessage() + "."); + } + + } + ); + } + + @Override + protected void handlePost(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { + notImplemented(channel, Method.POST); + } + + @Override + protected void handleGet(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { + notImplemented(channel, Method.GET); + } + + @Override + protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { + notImplemented(channel, Method.PUT); + } + + @Override + protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { + return new NoOpValidator(request, ref, this.settings, param); + } + + @Override + protected String getResourceName() { + // not needed + return null; + } + + @Override protected CType getConfigName() { - return null; - } + return null; + } - @Override - protected void consumeParameters(final RestRequest request) { - // not needed - } + @Override + protected void consumeParameters(final RestRequest request) { + // not needed + } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java index 6c1169f35a..3e041961b0 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/InternalUsersApiAction.java @@ -52,10 +52,11 @@ public class InternalUsersApiAction extends PatchableResourceApiAction { static final List RESTRICTED_FROM_USERNAME = ImmutableList.of( - ":" // Not allowed in basic auth, see https://stackoverflow.com/a/33391003/533057 + ":" // Not allowed in basic auth, see https://stackoverflow.com/a/33391003/533057 ); - private static final List routes = addRoutesPrefix(ImmutableList.of( + private static final List routes = addRoutesPrefix( + ImmutableList.of( new Route(Method.GET, "/user/{name}"), new Route(Method.GET, "/user/"), new Route(Method.POST, "/user/{name}/authtoken"), @@ -70,24 +71,36 @@ public class InternalUsersApiAction extends PatchableResourceApiAction { new Route(Method.PUT, "/internalusers/{name}"), new Route(Method.PATCH, "/internalusers/"), new Route(Method.PATCH, "/internalusers/{name}") - )); + ) + ); UserService userService; @Inject - public InternalUsersApiAction(final Settings settings, final Path configPath, final RestController controller, - final Client client, final AdminDNs adminDNs, final ConfigurationRepository cl, - final ClusterService cs, final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, - ThreadPool threadPool, UserService userService, AuditLog auditLog) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, - auditLog); + public InternalUsersApiAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + ThreadPool threadPool, + UserService userService, + AuditLog auditLog + ) { + super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); this.userService = userService; } @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName) { + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) { return true; } @@ -102,7 +115,8 @@ protected Endpoint getEndpoint() { } @Override - protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { + protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { final String username = request.param("name"); @@ -118,7 +132,7 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C // Don't allow user to add non-existent role or a role for which role-mapping is hidden or reserved final List securityRoles = securityJsonNode.get("opendistro_security_roles").asList(); if (securityRoles != null) { - for (final String role: securityRoles) { + for (final String role : securityRoles) { if (!isValidRolesMapping(channel, role)) { return; } @@ -139,12 +153,10 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C } ((ObjectNode) content).put("name", username); internalUsersConfiguration = userService.createOrUpdateAccount((ObjectNode) content); - } - catch (UserServiceException ex) { + } catch (UserServiceException ex) { badRequestResponse(channel, ex.getMessage()); return; - } - catch (IOException ex) { + } catch (IOException ex) { throw new IOException(ex); } @@ -153,8 +165,10 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C // sanity check, this should usually not happen final String hash = ((Hashed) internalUsersConfiguration.getCEntry(username)).getHash(); if (hash == null || hash.length() == 0) { - internalErrorResponse(channel, - "Existing user " + username + " has no password, and no new password or hash was specified."); + internalErrorResponse( + channel, + "Existing user " + username + " has no password, and no new password or hash was specified." + ); return; } contentAsNode.put("hash", hash); @@ -163,23 +177,27 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C internalUsersConfiguration.remove(username); // checks complete, create or update the user - Object userData = DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass()); + Object userData = DefaultObjectMapper.readTree(contentAsNode, internalUsersConfiguration.getImplementingClass()); internalUsersConfiguration.putCObject(username, userData); + saveAndUpdateConfigs( + this.securityIndexName, + client, + CType.INTERNALUSERS, + internalUsersConfiguration, + new OnSucessActionListener(channel) { + + @Override + public void onResponse(IndexResponse response) { + if (userExisted) { + successResponse(channel, "'" + username + "' updated."); + } else { + createdResponse(channel, "'" + username + "' created."); + } - saveAndUpdateConfigs(this.securityIndexName,client, CType.INTERNALUSERS, internalUsersConfiguration, new OnSucessActionListener(channel) { - - - @Override - public void onResponse(IndexResponse response) { - if (userExisted) { - successResponse(channel, "'" + username + "' updated."); - } else { - createdResponse(channel, "'" + username + "' created."); } - } - }); + ); } /** @@ -192,7 +210,7 @@ public void onResponse(IndexResponse response) { * @throws IOException when parsing of configuration files fails (should not happen) */ @Override - protected void handlePost(final RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException{ + protected void handlePost(final RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { final String username = request.param("name"); @@ -215,7 +233,10 @@ protected void handlePost(final RestChannel channel, RestRequest request, Client String authToken = ""; try { - if (request.uri().contains("/internalusers/" + username + "/authtoken") && request.uri().endsWith("/authtoken")) { // Handle auth token fetching + if (request.uri().contains("/internalusers/" + username + "/authtoken") && request.uri().endsWith("/authtoken")) { // Handle + // auth + // token + // fetching authToken = userService.generateAuthToken(username); } else { // Not an auth token request @@ -223,22 +244,20 @@ protected void handlePost(final RestChannel channel, RestRequest request, Client notImplemented(channel, Method.POST); return; } - } catch (UserServiceException ex) { + } catch (UserServiceException ex) { badRequestResponse(channel, ex.getMessage()); return; - } - catch (IOException ex) { + } catch (IOException ex) { throw new IOException(ex); } if (!authToken.isEmpty()) { - createdResponse(channel, "'" + username + "' authtoken generated " + authToken); + createdResponse(channel, "'" + username + "' authtoken generated " + authToken); } else { badRequestResponse(channel, "'" + username + "' authtoken failed to be created."); } } - @Override protected void filter(SecurityDynamicConfiguration builder) { super.filter(builder); @@ -249,8 +268,13 @@ protected void filter(SecurityDynamicConfiguration builder) { } @Override - protected AbstractConfigurationValidator postProcessApplyPatchResult(RestChannel channel, RestRequest request, JsonNode existingResourceAsJsonNode, - JsonNode updatedResourceAsJsonNode, String resourceName) { + protected AbstractConfigurationValidator postProcessApplyPatchResult( + RestChannel channel, + RestRequest request, + JsonNode existingResourceAsJsonNode, + JsonNode updatedResourceAsJsonNode, + String resourceName + ) { AbstractConfigurationValidator retVal = null; JsonNode passwordNode = updatedResourceAsJsonNode.get("password"); diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java index d252515f72..108e29b980 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/MigrateApiAction.java @@ -71,14 +71,22 @@ // CS-ENFORCE-SINGLE public class MigrateApiAction extends AbstractApiAction { - private static final List routes = addRoutesPrefix(Collections.singletonList( - new Route(Method.POST, "/migrate") - )); + private static final List routes = addRoutesPrefix(Collections.singletonList(new Route(Method.POST, "/migrate"))); @Inject - public MigrateApiAction(final Settings settings, final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDNs, final ConfigurationRepository cl, final ClusterService cs, final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { + public MigrateApiAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); } @@ -93,9 +101,11 @@ protected Endpoint getEndpoint() { } @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName) { + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) { return true; } @@ -111,12 +121,22 @@ protected void handlePost(RestChannel channel, RestRequest request, Client clien } final SecurityDynamicConfiguration configV6 = (SecurityDynamicConfiguration) loadedConfig; - final SecurityDynamicConfiguration actionGroupsV6 = (SecurityDynamicConfiguration) load(CType.ACTIONGROUPS, true); - final SecurityDynamicConfiguration internalUsersV6 = (SecurityDynamicConfiguration) load(CType.INTERNALUSERS, true); + final SecurityDynamicConfiguration actionGroupsV6 = (SecurityDynamicConfiguration) load( + CType.ACTIONGROUPS, + true + ); + final SecurityDynamicConfiguration internalUsersV6 = (SecurityDynamicConfiguration) load( + CType.INTERNALUSERS, + true + ); final SecurityDynamicConfiguration rolesV6 = (SecurityDynamicConfiguration) load(CType.ROLES, true); - final SecurityDynamicConfiguration rolesmappingV6 = (SecurityDynamicConfiguration) load(CType.ROLESMAPPING, true); + final SecurityDynamicConfiguration rolesmappingV6 = (SecurityDynamicConfiguration) load( + CType.ROLESMAPPING, + true + ); final SecurityDynamicConfiguration nodesDnV6 = (SecurityDynamicConfiguration) load(CType.NODESDN, true); - final SecurityDynamicConfiguration whitelistingSettingV6 = (SecurityDynamicConfiguration) load(CType.WHITELIST, true); + final SecurityDynamicConfiguration whitelistingSettingV6 = (SecurityDynamicConfiguration< + WhitelistingSettings>) load(CType.WHITELIST, true); final SecurityDynamicConfiguration auditConfigV6 = (SecurityDynamicConfiguration) load(CType.AUDIT, true); final ImmutableList.Builder> builder = ImmutableList.builder(); @@ -127,21 +147,29 @@ protected void handlePost(RestChannel channel, RestRequest request, Client clien builder.add(configV7); final SecurityDynamicConfiguration internalUsersV7 = Migration.migrateInternalUsers(internalUsersV6); builder.add(internalUsersV7); - final Tuple, SecurityDynamicConfiguration> rolesTenantsV7 = Migration.migrateRoles(rolesV6, - rolesmappingV6); + final Tuple, SecurityDynamicConfiguration> rolesTenantsV7 = Migration.migrateRoles( + rolesV6, + rolesmappingV6 + ); builder.add(rolesTenantsV7.v1()); builder.add(rolesTenantsV7.v2()); final SecurityDynamicConfiguration rolesmappingV7 = Migration.migrateRoleMappings(rolesmappingV6); builder.add(rolesmappingV7); final SecurityDynamicConfiguration nodesDnV7 = Migration.migrateNodesDn(nodesDnV6); builder.add(nodesDnV7); - final SecurityDynamicConfiguration whitelistingSettingV7 = Migration.migrateWhitelistingSetting(whitelistingSettingV6); + final SecurityDynamicConfiguration whitelistingSettingV7 = Migration.migrateWhitelistingSetting( + whitelistingSettingV6 + ); builder.add(whitelistingSettingV7); final SecurityDynamicConfiguration auditConfigV7 = Migration.migrateAudit(auditConfigV6); builder.add(auditConfigV7); final int replicas = cs.state().metadata().index(securityIndexName).getNumberOfReplicas(); - final String autoExpandReplicas = cs.state().metadata().index(securityIndexName).getSettings().get(IndexMetadata.SETTING_AUTO_EXPAND_REPLICAS); + final String autoExpandReplicas = cs.state() + .metadata() + .index(securityIndexName) + .getSettings() + .get(IndexMetadata.SETTING_AUTO_EXPAND_REPLICAS); final Builder securityIndexSettings = Settings.builder(); @@ -161,57 +189,78 @@ public void onResponse(AcknowledgedResponse response) { if (response.isAcknowledged()) { log.debug("opendistro_security index deleted successfully"); - client.admin().indices().prepareCreate(securityIndexName).setSettings(securityIndexSettings) - .execute(new ActionListener() { - - @Override - public void onResponse(CreateIndexResponse response) { - final List> dynamicConfigurations = builder.build(); - final ImmutableList.Builder cTypes = ImmutableList.builderWithExpectedSize(dynamicConfigurations.size()); - final BulkRequestBuilder br = client.prepareBulk(securityIndexName); - br.setRefreshPolicy(RefreshPolicy.IMMEDIATE); - try { - for (SecurityDynamicConfiguration dynamicConfiguration : dynamicConfigurations) { - final String id = dynamicConfiguration.getCType().toLCString(); - final BytesReference xContent = XContentHelper.toXContent(dynamicConfiguration, XContentType.JSON, false); - br.add(new IndexRequest().id(id).source(id, xContent)); - cTypes.add(id); - } - } catch (final IOException e1) { - log.error("Unable to create bulk request " + e1, e1); - internalErrorResponse(channel, "Unable to create bulk request."); - return; + client.admin() + .indices() + .prepareCreate(securityIndexName) + .setSettings(securityIndexSettings) + .execute(new ActionListener() { + + @Override + public void onResponse(CreateIndexResponse response) { + final List> dynamicConfigurations = builder.build(); + final ImmutableList.Builder cTypes = ImmutableList.builderWithExpectedSize( + dynamicConfigurations.size() + ); + final BulkRequestBuilder br = client.prepareBulk(securityIndexName); + br.setRefreshPolicy(RefreshPolicy.IMMEDIATE); + try { + for (SecurityDynamicConfiguration dynamicConfiguration : dynamicConfigurations) { + final String id = dynamicConfiguration.getCType().toLCString(); + final BytesReference xContent = XContentHelper.toXContent( + dynamicConfiguration, + XContentType.JSON, + false + ); + br.add(new IndexRequest().id(id).source(id, xContent)); + cTypes.add(id); } + } catch (final IOException e1) { + log.error("Unable to create bulk request " + e1, e1); + internalErrorResponse(channel, "Unable to create bulk request."); + return; + } - br.execute(new ConfigUpdatingActionListener(cTypes.build().toArray(new String[0]), client, new ActionListener() { + br.execute( + new ConfigUpdatingActionListener( + cTypes.build().toArray(new String[0]), + client, + new ActionListener() { + + @Override + public void onResponse(BulkResponse response) { + if (response.hasFailures()) { + log.error( + "Unable to upload migrated configuration because of " + + response.buildFailureMessage() + ); + internalErrorResponse( + channel, + "Unable to upload migrated configuration (bulk index failed)." + ); + } else { + log.debug("Migration completed"); + successResponse(channel, "Migration completed."); + } - @Override - public void onResponse(BulkResponse response) { - if (response.hasFailures()) { - log.error("Unable to upload migrated configuration because of " + response.buildFailureMessage()); - internalErrorResponse(channel, "Unable to upload migrated configuration (bulk index failed)."); - } else { - log.debug("Migration completed"); - successResponse(channel, "Migration completed."); } + @Override + public void onFailure(Exception e) { + log.error("Unable to upload migrated configuration because of " + e, e); + internalErrorResponse(channel, "Unable to upload migrated configuration."); + } } + ) + ); - @Override - public void onFailure(Exception e) { - log.error("Unable to upload migrated configuration because of " + e, e); - internalErrorResponse(channel, "Unable to upload migrated configuration."); - } - })); - - } + } - @Override - public void onFailure(Exception e) { - log.error("Unable to create opendistro_security index because of " + e, e); - internalErrorResponse(channel, "Unable to create opendistro_security index."); - } - }); + @Override + public void onFailure(Exception e) { + log.error("Unable to create opendistro_security index because of " + e, e); + internalErrorResponse(channel, "Unable to create opendistro_security index."); + } + }); } else { log.error("Unable to create opendistro_security index."); @@ -228,17 +277,20 @@ public void onFailure(Exception e) { } @Override - protected void handleDelete(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { + protected void handleDelete(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { notImplemented(channel, Method.POST); } @Override - protected void handleGet(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { + protected void handleGet(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { notImplemented(channel, Method.GET); } @Override - protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { + protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { notImplemented(channel, Method.PUT); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java index e5ec82c245..ef5be4634f 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/MultiTenancyConfigApiAction.java @@ -51,20 +51,16 @@ import static org.opensearch.rest.RestRequest.Method.PUT; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; - public class MultiTenancyConfigApiAction extends AbstractApiAction { private static final List ROUTES = addRoutesPrefix( - ImmutableList.of( - new Route(GET, "/tenancy/config"), - new Route(PUT, "/tenancy/config") - ) + ImmutableList.of(new Route(GET, "/tenancy/config"), new Route(PUT, "/tenancy/config")) ); private final static Set ACCEPTABLE_DEFAULT_TENANTS = ImmutableSet.of( - ConfigConstants.TENANCY_GLOBAL_TENANT_DEFAULT_NAME, - ConfigConstants.TENANCY_GLOBAL_TENANT_NAME, - ConfigConstants.TENANCY_PRIVATE_TENANT_NAME + ConfigConstants.TENANCY_GLOBAL_TENANT_DEFAULT_NAME, + ConfigConstants.TENANCY_GLOBAL_TENANT_NAME, + ConfigConstants.TENANCY_PRIVATE_TENANT_NAME ); @Override @@ -78,12 +74,18 @@ public List routes() { } public MultiTenancyConfigApiAction( - final Settings settings, final Path configPath, - final RestController controller, final Client client, - final AdminDNs adminDNs, final ConfigurationRepository cl, - final ClusterService cs, final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, final ThreadPool threadPool, - final AuditLog auditLog) { + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + final ThreadPool threadPool, + final AuditLog auditLog + ) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); } @@ -108,31 +110,25 @@ protected CType getConfigName() { } @Override - protected void handleDelete(final RestChannel channel, - final RestRequest request, - final Client client, - final JsonNode content) throws IOException { + protected void handleDelete(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { notImplemented(channel, RestRequest.Method.DELETE); } private void multitenancyResponse(final ConfigV7 config, final RestChannel channel) { try (final XContentBuilder contentBuilder = channel.newBuilder()) { channel.sendResponse( - new BytesRestResponse( - RestStatus.OK, - contentBuilder - .startObject() - .field( - MultiTenancyConfigValidator.DEFAULT_TENANT_JSON_PROPERTY, - config.dynamic.kibana.default_tenant - ).field( - MultiTenancyConfigValidator.PRIVATE_TENANT_ENABLED_JSON_PROPERTY, - config.dynamic.kibana.private_tenant_enabled - ).field( - MultiTenancyConfigValidator.MULTITENANCY_ENABLED_JSON_PROPERTY, - config.dynamic.kibana.multitenancy_enabled - ).endObject() - ) + new BytesRestResponse( + RestStatus.OK, + contentBuilder.startObject() + .field(MultiTenancyConfigValidator.DEFAULT_TENANT_JSON_PROPERTY, config.dynamic.kibana.default_tenant) + .field( + MultiTenancyConfigValidator.PRIVATE_TENANT_ENABLED_JSON_PROPERTY, + config.dynamic.kibana.private_tenant_enabled + ) + .field(MultiTenancyConfigValidator.MULTITENANCY_ENABLED_JSON_PROPERTY, config.dynamic.kibana.multitenancy_enabled) + .endObject() + ) ); } catch (final Exception e) { internalErrorResponse(channel, e.getMessage()); @@ -140,61 +136,49 @@ private void multitenancyResponse(final ConfigV7 config, final RestChannel chann } } - @Override - protected void handleGet(final RestChannel channel, - final RestRequest request, - final Client client, - final JsonNode content) throws IOException { + protected void handleGet(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { final SecurityDynamicConfiguration dynamicConfiguration = load(CType.CONFIG, false); final ConfigV7 config = (ConfigV7) dynamicConfiguration.getCEntry(CType.CONFIG.toLCString()); multitenancyResponse(config, channel); } @Override - protected void handlePut(final RestChannel channel, - final RestRequest request, - final Client client, - final JsonNode content) throws IOException { - final SecurityDynamicConfiguration dynamicConfiguration = (SecurityDynamicConfiguration) - load(CType.CONFIG, false); + protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { + final SecurityDynamicConfiguration dynamicConfiguration = (SecurityDynamicConfiguration) load( + CType.CONFIG, + false + ); final ConfigV7 config = dynamicConfiguration.getCEntry(CType.CONFIG.toLCString()); updateAndValidatesValues(config, content); dynamicConfiguration.putCEntry(CType.CONFIG.toLCString(), config); - saveAndUpdateConfigs( - this.securityIndexName, - client, - getConfigName(), - dynamicConfiguration, - new OnSucessActionListener<>(channel) { - @Override - public void onResponse(IndexResponse response) { - multitenancyResponse(config, channel); - } - } - ); + saveAndUpdateConfigs(this.securityIndexName, client, getConfigName(), dynamicConfiguration, new OnSucessActionListener<>(channel) { + @Override + public void onResponse(IndexResponse response) { + multitenancyResponse(config, channel); + } + }); } private void updateAndValidatesValues(final ConfigV7 config, final JsonNode jsonContent) { if (Objects.nonNull(jsonContent.findValue(MultiTenancyConfigValidator.DEFAULT_TENANT_JSON_PROPERTY))) { - config.dynamic.kibana.default_tenant = - jsonContent.findValue(MultiTenancyConfigValidator.DEFAULT_TENANT_JSON_PROPERTY).asText(); + config.dynamic.kibana.default_tenant = jsonContent.findValue(MultiTenancyConfigValidator.DEFAULT_TENANT_JSON_PROPERTY).asText(); } if (Objects.nonNull(jsonContent.findValue(MultiTenancyConfigValidator.PRIVATE_TENANT_ENABLED_JSON_PROPERTY))) { - config.dynamic.kibana.private_tenant_enabled = - jsonContent.findValue(MultiTenancyConfigValidator.PRIVATE_TENANT_ENABLED_JSON_PROPERTY).booleanValue(); + config.dynamic.kibana.private_tenant_enabled = jsonContent.findValue( + MultiTenancyConfigValidator.PRIVATE_TENANT_ENABLED_JSON_PROPERTY + ).booleanValue(); } if (Objects.nonNull(jsonContent.findValue(MultiTenancyConfigValidator.MULTITENANCY_ENABLED_JSON_PROPERTY))) { - config.dynamic.kibana.multitenancy_enabled = - jsonContent.findValue(MultiTenancyConfigValidator.MULTITENANCY_ENABLED_JSON_PROPERTY).asBoolean(); + config.dynamic.kibana.multitenancy_enabled = jsonContent.findValue( + MultiTenancyConfigValidator.MULTITENANCY_ENABLED_JSON_PROPERTY + ).asBoolean(); } - final String defaultTenant = - Optional.ofNullable(config.dynamic.kibana.default_tenant) - .map(String::toLowerCase) - .orElse(""); + final String defaultTenant = Optional.ofNullable(config.dynamic.kibana.default_tenant).map(String::toLowerCase).orElse(""); - if (!config.dynamic.kibana.private_tenant_enabled - && ConfigConstants.TENANCY_PRIVATE_TENANT_NAME.equals(defaultTenant)) { + if (!config.dynamic.kibana.private_tenant_enabled && ConfigConstants.TENANCY_PRIVATE_TENANT_NAME.equals(defaultTenant)) { throw new IllegalArgumentException("Private tenant can not be disabled if it is the default tenant."); } @@ -202,15 +186,17 @@ private void updateAndValidatesValues(final ConfigV7 config, final JsonNode json return; } - final Set availableTenants = - cl.getConfiguration(CType.TENANTS) - .getCEntries() - .keySet() - .stream() - .map(String::toLowerCase) - .collect(Collectors.toSet()); + final Set availableTenants = cl.getConfiguration(CType.TENANTS) + .getCEntries() + .keySet() + .stream() + .map(String::toLowerCase) + .collect(Collectors.toSet()); if (!availableTenants.contains(defaultTenant)) { - throw new IllegalArgumentException(config.dynamic.kibana.default_tenant + " can not be set to default tenant. Default tenant should be selected from one of the available tenants."); + throw new IllegalArgumentException( + config.dynamic.kibana.default_tenant + + " can not be set to default tenant. Default tenant should be selected from one of the available tenants." + ); } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java index 22897b8305..6d44e4073c 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/NodesDnApiAction.java @@ -60,27 +60,41 @@ public class NodesDnApiAction extends PatchableResourceApiAction { public static final String STATIC_OPENSEARCH_YML_NODES_DN = "STATIC_OPENSEARCH_YML_NODES_DN"; private final List staticNodesDnFromEsYml; - private static final List routes = addRoutesPrefix(ImmutableList.of( + private static final List routes = addRoutesPrefix( + ImmutableList.of( new Route(Method.GET, "/nodesdn/{name}"), new Route(Method.GET, "/nodesdn/"), new Route(Method.DELETE, "/nodesdn/{name}"), new Route(Method.PUT, "/nodesdn/{name}"), new Route(Method.PATCH, "/nodesdn/"), new Route(Method.PATCH, "/nodesdn/{name}") - )); + ) + ); @Inject - public NodesDnApiAction(final Settings settings, final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDNs, final ConfigurationRepository cl, final ClusterService cs, - final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { + public NodesDnApiAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); this.staticNodesDnFromEsYml = settings.getAsList(ConfigConstants.SECURITY_NODES_DN, Collections.emptyList()); } @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName) { + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) { return true; } @@ -144,7 +158,7 @@ private void putStaticEntry(SecurityDynamicConfiguration configuration) { if (NodesDn.class.equals(configuration.getImplementingClass())) { NodesDn nodesDn = new NodesDn(); nodesDn.setNodesDn(staticNodesDnFromEsYml); - ((SecurityDynamicConfiguration)configuration).putCEntry(STATIC_OPENSEARCH_YML_NODES_DN, nodesDn); + ((SecurityDynamicConfiguration) configuration).putCEntry(STATIC_OPENSEARCH_YML_NODES_DN, nodesDn); } else { throw new RuntimeException("Unknown class type - " + configuration.getImplementingClass()); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java index 796a25fa9f..e6d1f1744a 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/PatchableResourceApiAction.java @@ -54,16 +54,23 @@ public abstract class PatchableResourceApiAction extends AbstractApiAction { protected final Logger log = LogManager.getLogger(this.getClass()); - public PatchableResourceApiAction(Settings settings, Path configPath, RestController controller, Client client, - AdminDNs adminDNs, ConfigurationRepository cl, ClusterService cs, - PrincipalExtractor principalExtractor, PrivilegesEvaluator evaluator, ThreadPool threadPool, - AuditLog auditLog) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, - auditLog); + public PatchableResourceApiAction( + Settings settings, + Path configPath, + RestController controller, + Client client, + AdminDNs adminDNs, + ConfigurationRepository cl, + ClusterService cs, + PrincipalExtractor principalExtractor, + PrivilegesEvaluator evaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { + super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); } - private void handlePatch(RestChannel channel, final RestRequest request, final Client client) - throws IOException { + private void handlePatch(RestChannel channel, final RestRequest request, final Client client) throws IOException { if (request.getXContentType() != XContentType.JSON) { badRequestResponse(channel, "PATCH accepts only application/json"); return; @@ -103,8 +110,15 @@ private void handlePatch(RestChannel channel, final RestRequest request, final C } } - private void handleSinglePatch(RestChannel channel, RestRequest request, Client client, String name, - SecurityDynamicConfiguration existingConfiguration, ObjectNode existingAsObjectNode, JsonNode jsonPatch) throws IOException { + private void handleSinglePatch( + RestChannel channel, + RestRequest request, + Client client, + String name, + SecurityDynamicConfiguration existingConfiguration, + ObjectNode existingAsObjectNode, + JsonNode jsonPatch + ) throws IOException { if (!isWriteable(channel, existingConfiguration, name)) { return; } @@ -126,9 +140,15 @@ private void handleSinglePatch(RestChannel channel, RestRequest request, Client return; } - AbstractConfigurationValidator originalValidator = postProcessApplyPatchResult(channel, request, existingResourceAsJsonNode, patchedResourceAsJsonNode, name); + AbstractConfigurationValidator originalValidator = postProcessApplyPatchResult( + channel, + request, + existingResourceAsJsonNode, + patchedResourceAsJsonNode, + name + ); - if(originalValidator != null) { + if (originalValidator != null) { if (!originalValidator.validate()) { request.params().clear(); badRequestResponse(channel, originalValidator); @@ -152,8 +172,13 @@ private void handleSinglePatch(RestChannel channel, RestRequest request, Client JsonNode updatedAsJsonNode = existingAsObjectNode.deepCopy().set(name, patchedResourceAsJsonNode); - SecurityDynamicConfiguration mdc = SecurityDynamicConfiguration.fromNode(updatedAsJsonNode, existingConfiguration.getCType() - , existingConfiguration.getVersion(), existingConfiguration.getSeqNo(), existingConfiguration.getPrimaryTerm()); + SecurityDynamicConfiguration mdc = SecurityDynamicConfiguration.fromNode( + updatedAsJsonNode, + existingConfiguration.getCType(), + existingConfiguration.getVersion(), + existingConfiguration.getSeqNo(), + existingConfiguration.getPrimaryTerm() + ); if (existingConfiguration.getCType().equals(CType.ACTIONGROUPS)) { if (hasActionGroupSelfReference(mdc, name)) { @@ -162,18 +187,24 @@ private void handleSinglePatch(RestChannel channel, RestRequest request, Client } } - saveAndUpdateConfigs(this.securityIndexName,client, getConfigName(), mdc, new OnSucessActionListener(channel){ + saveAndUpdateConfigs(this.securityIndexName, client, getConfigName(), mdc, new OnSucessActionListener(channel) { @Override public void onResponse(IndexResponse response) { successResponse(channel, "'" + name + "' updated."); } - }); + }); } - private void handleBulkPatch(RestChannel channel, RestRequest request, Client client, - SecurityDynamicConfiguration existingConfiguration, ObjectNode existingAsObjectNode, JsonNode jsonPatch) throws IOException { + private void handleBulkPatch( + RestChannel channel, + RestRequest request, + Client client, + SecurityDynamicConfiguration existingConfiguration, + ObjectNode existingAsObjectNode, + JsonNode jsonPatch + ) throws IOException { JsonNode patchedAsJsonNode; @@ -193,16 +224,21 @@ private void handleBulkPatch(RestChannel channel, RestRequest request, Client cl } } - for (Iterator fieldNamesIter = patchedAsJsonNode.fieldNames(); fieldNamesIter.hasNext();) { String resourceName = fieldNamesIter.next(); JsonNode oldResource = existingAsObjectNode.get(resourceName); JsonNode patchedResource = patchedAsJsonNode.get(resourceName); - AbstractConfigurationValidator originalValidator = postProcessApplyPatchResult(channel, request, oldResource, patchedResource, resourceName); + AbstractConfigurationValidator originalValidator = postProcessApplyPatchResult( + channel, + request, + oldResource, + patchedResource, + resourceName + ); - if(originalValidator != null) { + if (originalValidator != null) { if (!originalValidator.validate()) { request.params().clear(); badRequestResponse(channel, originalValidator); @@ -232,25 +268,30 @@ private void handleBulkPatch(RestChannel channel, RestRequest request, Client cl } } } - SecurityDynamicConfiguration mdc = SecurityDynamicConfiguration.fromNode(patchedAsJsonNode, existingConfiguration.getCType() - , existingConfiguration.getVersion(), existingConfiguration.getSeqNo(), existingConfiguration.getPrimaryTerm()); + SecurityDynamicConfiguration mdc = SecurityDynamicConfiguration.fromNode( + patchedAsJsonNode, + existingConfiguration.getCType(), + existingConfiguration.getVersion(), + existingConfiguration.getSeqNo(), + existingConfiguration.getPrimaryTerm() + ); if (existingConfiguration.getCType().equals(CType.ACTIONGROUPS)) { for (String actiongroup : mdc.getCEntries().keySet()) { - if(hasActionGroupSelfReference(mdc, actiongroup)) { + if (hasActionGroupSelfReference(mdc, actiongroup)) { badRequestResponse(channel, actiongroup + " cannot be an allowed_action of itself"); return; } } } - saveAndUpdateConfigs(this.securityIndexName,client, getConfigName(), mdc, new OnSucessActionListener(channel) { + saveAndUpdateConfigs(this.securityIndexName, client, getConfigName(), mdc, new OnSucessActionListener(channel) { @Override public void onResponse(IndexResponse response) { successResponse(channel, "Resource updated."); } - }); + }); } @@ -258,14 +299,19 @@ private JsonNode applyPatch(JsonNode jsonPatch, JsonNode existingResourceAsJsonN return JsonPatch.apply(jsonPatch, existingResourceAsJsonNode); } - protected AbstractConfigurationValidator postProcessApplyPatchResult(RestChannel channel, RestRequest request, JsonNode existingResourceAsJsonNode, JsonNode updatedResourceAsJsonNode, String resourceName) { + protected AbstractConfigurationValidator postProcessApplyPatchResult( + RestChannel channel, + RestRequest request, + JsonNode existingResourceAsJsonNode, + JsonNode updatedResourceAsJsonNode, + String resourceName + ) { // do nothing by default return null; } @Override - protected void handleApiRequest(RestChannel channel, final RestRequest request, final Client client) - throws IOException { + protected void handleApiRequest(RestChannel channel, final RestRequest request, final Client client) throws IOException { if (request.method() == Method.PATCH) { handlePatch(channel, request, client); @@ -274,10 +320,10 @@ protected void handleApiRequest(RestChannel channel, final RestRequest request, } } - private AbstractConfigurationValidator getValidator(RestRequest request, JsonNode patchedResource) - throws JsonProcessingException { + private AbstractConfigurationValidator getValidator(RestRequest request, JsonNode patchedResource) throws JsonProcessingException { BytesReference patchedResourceAsByteReference = new BytesArray( - DefaultObjectMapper.objectMapper.writeValueAsString(patchedResource).getBytes(StandardCharsets.UTF_8)); + DefaultObjectMapper.objectMapper.writeValueAsString(patchedResource).getBytes(StandardCharsets.UTF_8) + ); return getValidator(request, patchedResourceAsByteReference); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/PermissionsInfoAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/PermissionsInfoAction.java index d07be301bd..f57c4eb59c 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/PermissionsInfoAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/PermissionsInfoAction.java @@ -49,85 +49,104 @@ * Provides the evaluated REST API permissions for the currently logged in user */ public class PermissionsInfoAction extends BaseRestHandler { - private static final List routes = addRoutesPrefix(Collections.singletonList( - new Route(Method.GET, "/permissionsinfo") - )); - - private final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator; - private final ThreadPool threadPool; - private final PrivilegesEvaluator privilegesEvaluator; - private final ConfigurationRepository configurationRepository; - - protected PermissionsInfoAction(final Settings settings, final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDNs, final ConfigurationRepository configurationRepository, final ClusterService cs, - final PrincipalExtractor principalExtractor, final PrivilegesEvaluator privilegesEvaluator, ThreadPool threadPool, AuditLog auditLog) { - super(); - this.threadPool = threadPool; - this.privilegesEvaluator = privilegesEvaluator; - this.restApiPrivilegesEvaluator = new RestApiPrivilegesEvaluator(settings, adminDNs, privilegesEvaluator, principalExtractor, configPath, threadPool); - this.configurationRepository = configurationRepository; - } - - @Override - public String getName() { - return getClass().getSimpleName(); - } - - @Override - public List routes() { - return routes; - } - - @Override - protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { - switch (request.method()) { - case GET: - return handleGet(request, client); - default: - throw new IllegalArgumentException(request.method() + " not supported"); - } - } - - private RestChannelConsumer handleGet(RestRequest request, NodeClient client) throws IOException { + private static final List routes = addRoutesPrefix(Collections.singletonList(new Route(Method.GET, "/permissionsinfo"))); + + private final RestApiPrivilegesEvaluator restApiPrivilegesEvaluator; + private final ThreadPool threadPool; + private final PrivilegesEvaluator privilegesEvaluator; + private final ConfigurationRepository configurationRepository; + + protected PermissionsInfoAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository configurationRepository, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator privilegesEvaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { + super(); + this.threadPool = threadPool; + this.privilegesEvaluator = privilegesEvaluator; + this.restApiPrivilegesEvaluator = new RestApiPrivilegesEvaluator( + settings, + adminDNs, + privilegesEvaluator, + principalExtractor, + configPath, + threadPool + ); + this.configurationRepository = configurationRepository; + } + + @Override + public String getName() { + return getClass().getSimpleName(); + } + + @Override + public List routes() { + return routes; + } + + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + switch (request.method()) { + case GET: + return handleGet(request, client); + default: + throw new IllegalArgumentException(request.method() + " not supported"); + } + } + + private RestChannelConsumer handleGet(RestRequest request, NodeClient client) throws IOException { return new RestChannelConsumer() { @Override public void accept(RestChannel channel) throws Exception { - XContentBuilder builder = channel.newBuilder(); //NOSONAR + XContentBuilder builder = channel.newBuilder(); // NOSONAR BytesRestResponse response = null; try { - final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - final TransportAddress remoteAddress = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); - Set userRoles = privilegesEvaluator.mapRoles(user, remoteAddress); - Boolean hasApiAccess = restApiPrivilegesEvaluator.currentUserHasRestApiAccess(userRoles); - Map> disabledEndpoints = restApiPrivilegesEvaluator.getDisabledEndpointsForCurrentUser(user.getName(), userRoles); - if (!configurationRepository.isAuditHotReloadingEnabled()) { - disabledEndpoints.put(Endpoint.AUDIT, ImmutableList.copyOf(Method.values())); - } + final User user = threadPool.getThreadContext().getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final TransportAddress remoteAddress = threadPool.getThreadContext() + .getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); + Set userRoles = privilegesEvaluator.mapRoles(user, remoteAddress); + Boolean hasApiAccess = restApiPrivilegesEvaluator.currentUserHasRestApiAccess(userRoles); + Map> disabledEndpoints = restApiPrivilegesEvaluator.getDisabledEndpointsForCurrentUser( + user.getName(), + userRoles + ); + if (!configurationRepository.isAuditHotReloadingEnabled()) { + disabledEndpoints.put(Endpoint.AUDIT, ImmutableList.copyOf(Method.values())); + } builder.startObject(); - builder.field("user", user==null?null:user.toString()); - builder.field("user_name", user==null?null:user.getName()); //NOSONAR + builder.field("user", user == null ? null : user.toString()); + builder.field("user_name", user == null ? null : user.getName()); // NOSONAR builder.field("has_api_access", hasApiAccess); builder.startObject("disabled_endpoints"); - for(Entry> entry : disabledEndpoints.entrySet()) { - builder.field(entry.getKey().name(), entry.getValue()); + for (Entry> entry : disabledEndpoints.entrySet()) { + builder.field(entry.getKey().name(), entry.getValue()); } builder.endObject(); builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); } catch (final Exception e1) { e1.printStackTrace(); - builder = channel.newBuilder(); //NOSONAR + 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) { + if (builder != null) { builder.close(); } } @@ -136,6 +155,6 @@ public void accept(RestChannel channel) throws Exception { } }; - } + } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java index c8e44ee4ca..b0a0d828cc 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiAdminPrivilegesEvaluator.java @@ -41,14 +41,11 @@ public class RestApiAdminPrivilegesEvaluator { private final static String REST_API_PERMISSION_PREFIX = "restapi:admin"; - private final static String REST_ENDPOINT_PERMISSION_PATTERN = - REST_API_PERMISSION_PREFIX + "/%s"; + private final static String REST_ENDPOINT_PERMISSION_PATTERN = REST_API_PERMISSION_PREFIX + "/%s"; - private final static String REST_ENDPOINT_ACTION_PERMISSION_PATTERN = - REST_API_PERMISSION_PREFIX + "/%s/%s"; + private final static String REST_ENDPOINT_ACTION_PERMISSION_PATTERN = REST_API_PERMISSION_PREFIX + "/%s/%s"; - private final static WildcardMatcher REST_API_PERMISSION_PREFIX_MATCHER = - WildcardMatcher.from(REST_API_PERMISSION_PREFIX + "/*"); + private final static WildcardMatcher REST_API_PERMISSION_PREFIX_MATCHER = WildcardMatcher.from(REST_API_PERMISSION_PREFIX + "/*"); @FunctionalInterface public interface PermissionBuilder { @@ -61,25 +58,25 @@ default String build() { } - public final static Map ENDPOINTS_WITH_PERMISSIONS = - ImmutableMap.builder() - .put(Endpoint.ACTIONGROUPS, action -> buildEndpointPermission(Endpoint.ACTIONGROUPS)) - .put(Endpoint.ALLOWLIST, action -> buildEndpointPermission(Endpoint.ALLOWLIST)) - .put(Endpoint.INTERNALUSERS, action -> buildEndpointPermission(Endpoint.INTERNALUSERS)) - .put(Endpoint.NODESDN, action -> buildEndpointPermission(Endpoint.NODESDN)) - .put(Endpoint.ROLES, action -> buildEndpointPermission(Endpoint.ROLES)) - .put(Endpoint.ROLESMAPPING, action -> buildEndpointPermission(Endpoint.ROLESMAPPING)) - .put(Endpoint.TENANTS, action -> buildEndpointPermission(Endpoint.TENANTS)) - .put(Endpoint.SSL, action -> { - switch (action) { - case CERTS_INFO_ACTION: - return buildEndpointActionPermission(Endpoint.SSL, "certs/info"); - case RELOAD_CERTS_ACTION: - return buildEndpointActionPermission(Endpoint.SSL, "certs/reload"); - default: - return null; - } - }).build(); + public final static Map ENDPOINTS_WITH_PERMISSIONS = ImmutableMap.builder() + .put(Endpoint.ACTIONGROUPS, action -> buildEndpointPermission(Endpoint.ACTIONGROUPS)) + .put(Endpoint.ALLOWLIST, action -> buildEndpointPermission(Endpoint.ALLOWLIST)) + .put(Endpoint.INTERNALUSERS, action -> buildEndpointPermission(Endpoint.INTERNALUSERS)) + .put(Endpoint.NODESDN, action -> buildEndpointPermission(Endpoint.NODESDN)) + .put(Endpoint.ROLES, action -> buildEndpointPermission(Endpoint.ROLES)) + .put(Endpoint.ROLESMAPPING, action -> buildEndpointPermission(Endpoint.ROLESMAPPING)) + .put(Endpoint.TENANTS, action -> buildEndpointPermission(Endpoint.TENANTS)) + .put(Endpoint.SSL, action -> { + switch (action) { + case CERTS_INFO_ACTION: + return buildEndpointActionPermission(Endpoint.SSL, "certs/info"); + case RELOAD_CERTS_ACTION: + return buildEndpointActionPermission(Endpoint.SSL, "certs/reload"); + default: + return null; + } + }) + .build(); private final ThreadContext threadContext; @@ -90,10 +87,11 @@ default String build() { private final boolean restapiAdminEnabled; public RestApiAdminPrivilegesEvaluator( - final ThreadContext threadContext, - final PrivilegesEvaluator privilegesEvaluator, - final AdminDNs adminDNs, - final boolean restapiAdminEnabled) { + final ThreadContext threadContext, + final PrivilegesEvaluator privilegesEvaluator, + final AdminDNs adminDNs, + final boolean restapiAdminEnabled + ) { this.threadContext = threadContext; this.privilegesEvaluator = privilegesEvaluator; this.adminDNs = adminDNs; @@ -108,8 +106,10 @@ public boolean isCurrentUserRestApiAdminFor(final Endpoint endpoint, final Strin if (adminDNs.isAdmin(userAndRemoteAddress.getLeft())) { if (logger.isDebugEnabled()) { logger.debug( - "Security admin permissions required for endpoint {} but {} is not an admin", - endpoint, userAndRemoteAddress.getLeft().getName()); + "Security admin permissions required for endpoint {} but {} is not an admin", + endpoint, + userAndRemoteAddress.getLeft().getName() + ); } return true; } @@ -119,23 +119,23 @@ public boolean isCurrentUserRestApiAdminFor(final Endpoint endpoint, final Strin } final String permission = ENDPOINTS_WITH_PERMISSIONS.get(endpoint).build(action); final boolean hasAccess = privilegesEvaluator.hasRestAdminPermissions( - userAndRemoteAddress.getLeft(), - userAndRemoteAddress.getRight(), - permission + userAndRemoteAddress.getLeft(), + userAndRemoteAddress.getRight(), + permission ); if (logger.isDebugEnabled()) { logger.debug( - "User {} with permission {} {} access to endpoint {}", - userAndRemoteAddress.getLeft().getName(), - permission, - hasAccess ? "has" : "has no", - endpoint + "User {} with permission {} {} access to endpoint {}", + userAndRemoteAddress.getLeft().getName(), + permission, + hasAccess ? "has" : "has no", + endpoint ); logger.debug( - "{} set to {}. {} use access decision", - SECURITY_RESTAPI_ADMIN_ENABLED, - restapiAdminEnabled, - restapiAdminEnabled ? "Will" : "Will not" + "{} set to {}. {} use access decision", + SECURITY_RESTAPI_ADMIN_ENABLED, + restapiAdminEnabled, + restapiAdminEnabled ? "Will" : "Will not" ); } return hasAccess && restapiAdminEnabled; @@ -146,15 +146,9 @@ public boolean containsRestApiAdminPermissions(final Object configObject) { return false; } if (configObject instanceof RoleV7) { - return ((RoleV7) configObject) - .getCluster_permissions() - .stream() - .anyMatch(REST_API_PERMISSION_PREFIX_MATCHER); + return ((RoleV7) configObject).getCluster_permissions().stream().anyMatch(REST_API_PERMISSION_PREFIX_MATCHER); } else if (configObject instanceof ActionGroupsV7) { - return ((ActionGroupsV7) configObject) - .getAllowed_actions() - .stream() - .anyMatch(REST_API_PERMISSION_PREFIX_MATCHER); + return ((ActionGroupsV7) configObject).getAllowed_actions().stream().anyMatch(REST_API_PERMISSION_PREFIX_MATCHER); } else { return false; } @@ -165,17 +159,11 @@ public boolean isCurrentUserRestApiAdminFor(final Endpoint endpoint) { } private static String buildEndpointActionPermission(final Endpoint endpoint, final String action) { - return String.format( - REST_ENDPOINT_ACTION_PERMISSION_PATTERN, - endpoint.name().toLowerCase(Locale.ROOT), - action); + return String.format(REST_ENDPOINT_ACTION_PERMISSION_PATTERN, endpoint.name().toLowerCase(Locale.ROOT), action); } private static String buildEndpointPermission(final Endpoint endpoint) { - return String.format( - REST_ENDPOINT_PERMISSION_PATTERN, - endpoint.name().toLowerCase(Locale.ROOT) - ); + return String.format(REST_ENDPOINT_PERMISSION_PATTERN, endpoint.name().toLowerCase(Locale.ROOT)); } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java index 04b4b31c77..96787bb4f5 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RestApiPrivilegesEvaluator.java @@ -45,390 +45,431 @@ // TODO: Make Singleton? public class RestApiPrivilegesEvaluator { - protected final Logger logger = LogManager.getLogger(this.getClass()); - - private final AdminDNs adminDNs; - private final PrivilegesEvaluator privilegesEvaluator; - private final PrincipalExtractor principalExtractor; - private final Path configPath; - private final ThreadPool threadPool; - private final Settings settings; - - private final Set allowedRoles = new HashSet<>(); - - // endpoints per role, read and cached from settings. Changes here require a - // node restart, so it's save to cache. - private final Map>> disabledEndpointsForRoles = new HashMap<>(); - - // endpoints per user, evaluated and cached dynamically. Changes here - // require a node restart, so it's save to cache. - private final Map>> disabledEndpointsForUsers = new HashMap<>(); - - // globally disabled endpoints and methods, will always be forbidden - Map> globallyDisabledEndpoints = new HashMap<>(); - - // all endpoints and methods, will be returned for users that do not have any access at all - Map> allEndpoints = new HashMap<>(); - - private final Boolean roleBasedAccessEnabled; - - public RestApiPrivilegesEvaluator(final Settings settings, - final AdminDNs adminDNs, - final PrivilegesEvaluator privilegesEvaluator, - final PrincipalExtractor principalExtractor, - final Path configPath, - ThreadPool threadPool) { - - this.adminDNs = adminDNs; - this.privilegesEvaluator = privilegesEvaluator; - this.principalExtractor = principalExtractor; - this.configPath = configPath; - this.threadPool = threadPool; - this.settings = settings; - // set up - // all endpoints and methods - Map> allEndpoints = new HashMap<>(); - for(Endpoint endpoint : Endpoint.values()) { - List allMethods = new LinkedList<>(); - allMethods.addAll(Arrays.asList(Method.values())); - allEndpoints.put(endpoint, allMethods); - } - this.allEndpoints = Collections.unmodifiableMap(allEndpoints); - - // setup role based permissions - allowedRoles.addAll(settings.getAsList(ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED)); - - this.roleBasedAccessEnabled = !allowedRoles.isEmpty(); - - // globally disabled endpoints, disables access to Endpoint/Method combination for all roles - Settings globalSettings = settings.getAsSettings(ConfigConstants.SECURITY_RESTAPI_ENDPOINTS_DISABLED + ".global"); - if (!globalSettings.isEmpty()) { - globallyDisabledEndpoints = parseDisabledEndpoints(globalSettings); - } - - final boolean isDebugEnabled = logger.isDebugEnabled(); - if (isDebugEnabled) { - logger.debug("Globally disabled endpoints: {}", globallyDisabledEndpoints); - } - - for (String role : allowedRoles) { - Settings settingsForRole = settings.getAsSettings(ConfigConstants.SECURITY_RESTAPI_ENDPOINTS_DISABLED + "." + role); - if (settingsForRole.isEmpty()) { - if (isDebugEnabled) { - logger.debug("No disabled endpoints/methods for permitted role {} found, allowing all", role); - } - continue; - } - Map> disabledEndpointsForRole = parseDisabledEndpoints(settingsForRole); - if (!disabledEndpointsForRole.isEmpty()) { - disabledEndpointsForRoles.put(role, disabledEndpointsForRole); - } else { - logger.warn("Disabled endpoints/methods empty for role {}, please check configuration", role); - } - } - if (logger.isTraceEnabled()) { - logger.trace("Parsed permission set for endpoints: {}", disabledEndpointsForRoles); - } - } - - @SuppressWarnings({ "rawtypes" }) - private Map> parseDisabledEndpoints(Settings settings) { - - // Expects Setting like: 'ACTIONGROUPS=["GET", "POST"]' - if (settings == null || settings.isEmpty()) { - logger.error("Settings for disabled endpoint is null or empty: '{}', skipping.", settings); - return Collections.emptyMap(); - } - - final Map> disabledEndpoints = new HashMap>(); - - Map disabledEndpointsSettings = Utils.convertJsonToxToStructuredMap(settings); - - for (Entry value : disabledEndpointsSettings.entrySet()) { - // key is the endpoint, see if it is a valid one - String endpointString = value.getKey().toUpperCase(); - Endpoint endpoint = null; - try { - endpoint = Endpoint.valueOf(endpointString); - } catch (Exception e) { - logger.error("Unknown endpoint '{}' found in configuration, skipping.", endpointString); - continue; - } - // value must be non null - if (value.getValue() == null) { - logger.error("Disabled HTTP methods of endpoint '{}' is null, skipping.", endpointString); - continue; - } - - // value must be an array of methods - if (!(value.getValue() instanceof Collection)) { - logger.error("Disabled HTTP methods of endpoint '{}' must be an array, actually is '{}', skipping.", endpointString, (value.getValue().toString())); - } - List disabledMethods = new LinkedList<>(); - for (Object disabledMethodObj : (Collection) value.getValue()) { - if (disabledMethodObj == null) { - logger.error("Found null value in disabled HTTP methods of endpoint '{}', skipping.", endpointString); - continue; - } - - if (!(disabledMethodObj instanceof String)) { - logger.error("Found non-String value in disabled HTTP methods of endpoint '{}', skipping.", endpointString); - continue; - } - - String disabledMethodAsString = (String) disabledMethodObj; - - // Provide support for '*', means all methods - if (disabledMethodAsString.trim().equals("*")) { - disabledMethods.addAll(Arrays.asList(Method.values())); - break; - } - // no wild card, disabled method must be one of - // RestRequest.Method - Method disabledMethod = null; - try { - disabledMethod = Method.valueOf(disabledMethodAsString.toUpperCase()); - } catch (Exception e) { - logger.error("Invalid HTTP method '{}' found in disabled HTTP methods of endpoint '{}', skipping.", disabledMethodAsString.toUpperCase(), endpointString); - continue; - } - disabledMethods.add(disabledMethod); - } - - disabledEndpoints.put(endpoint, disabledMethods); - - } - return disabledEndpoints; - } - - /** - * Check if the current request is allowed to use the REST API and the - * requested end point. Using an admin certificate grants all permissions. A - * user/role can have restricted end points. - * - * @return an error message if user does not have access, null otherwise - * TODO: log failed attempt in audit log - */ - public String checkAccessPermissions(RestRequest request, Endpoint endpoint) throws IOException { - - if (logger.isDebugEnabled()) { - logger.debug("Checking admin access for endpoint {}, path {} and method {}", endpoint.name(), request.path(), request.method().name()); - } - - // Grant permission for Account endpoint. - // Return null to grant access. - if (endpoint == Endpoint.ACCOUNT) { - return null; - } - - String roleBasedAccessFailureReason = checkRoleBasedAccessPermissions(request, endpoint); - // Role based access granted - if (roleBasedAccessFailureReason == null) { - return null; - } - - String certBasedAccessFailureReason = checkAdminCertBasedAccessPermissions(request); - // TLS access granted, skip checking roles - if (certBasedAccessFailureReason == null) { - return null; - } - - - return constructAccessErrorMessage(roleBasedAccessFailureReason, certBasedAccessFailureReason); - } - - public Boolean currentUserHasRestApiAccess(Set userRoles) { - - // check if user has any role that grants access - return !Collections.disjoint(allowedRoles, userRoles); - - } - - public Map> getDisabledEndpointsForCurrentUser(String userPrincipal, Set userRoles) { - - final boolean isDebugEnabled = logger.isDebugEnabled(); - - // cache - if (disabledEndpointsForUsers.containsKey(userPrincipal)) { - return disabledEndpointsForUsers.get(userPrincipal); - } - - if (!currentUserHasRestApiAccess(userRoles)) { - return this.allEndpoints; - } - - // will contain the final list of disabled endpoints and methods - Map> finalEndpoints = new HashMap<>(); - - // List of all disabled endpoints for user. Disabled endpoints must be configured in all - // roles to take effect. If a role contains a disabled endpoint, but another role - // allows this endpoint (i.e. not contained in the disabled endpoints for this role), - // the access is allowed. - - // make list mutable - List remainingEndpoints = new LinkedList<>(Arrays.asList(Endpoint.values())); - - // only retain endpoints contained in all roles for user - boolean hasDisabledEndpoints = false; - for (String userRole : userRoles) { - Map> endpointsForRole = disabledEndpointsForRoles.get(userRole); - if (endpointsForRole == null || endpointsForRole.isEmpty()) { - continue; - } - Set disabledEndpoints = endpointsForRole.keySet(); - remainingEndpoints.retainAll(disabledEndpoints); - hasDisabledEndpoints = true; - } - - if (isDebugEnabled) { - logger.debug("Remaining endpoints for user {} after retaining all : {}", userPrincipal, remainingEndpoints); - } - - // if user does not have any disabled endpoints, only globally disabled endpoints apply - if (!hasDisabledEndpoints) { - - if (isDebugEnabled) { - logger.debug("No disabled endpoints for user {} at all, only globally disabledendpoints apply.", userPrincipal, remainingEndpoints); - } - disabledEndpointsForUsers.put(userPrincipal, addGloballyDisabledEndpoints(finalEndpoints)); - return finalEndpoints; - - } - - // one or more disabled remaining endpoints, keep only - // methods contained in all roles for each endpoint - for (Endpoint endpoint : remainingEndpoints) { - // make list mutable - List remainingMethodsForEndpoint = new LinkedList<>(Arrays.asList(Method.values())); - for (String userRole : userRoles) { - Map> endpoints = disabledEndpointsForRoles.get(userRole); - if (endpoints != null && !endpoints.isEmpty()) { - remainingMethodsForEndpoint.retainAll(endpoints.get(endpoint)); - } - } - - finalEndpoints.put(endpoint, remainingMethodsForEndpoint); - } - - if (isDebugEnabled) { - logger.debug("Disabled endpoints for user {} after retaining all : {}", userPrincipal, finalEndpoints); - } - - // add globally disabled endpoints and methods, will always be disabled - addGloballyDisabledEndpoints(finalEndpoints); - disabledEndpointsForUsers.put(userPrincipal, finalEndpoints); - - if (isDebugEnabled) { - logger.debug("Disabled endpoints for user {} after retaining all : {}", userPrincipal, disabledEndpointsForUsers.get(userPrincipal)); - } - - return disabledEndpointsForUsers.get(userPrincipal); - } - - private Map> addGloballyDisabledEndpoints(Map> endpoints) { - if(globallyDisabledEndpoints != null && !globallyDisabledEndpoints.isEmpty()) { - Set globalEndoints = globallyDisabledEndpoints.keySet(); - for(Endpoint endpoint : globalEndoints) { - endpoints.putIfAbsent(endpoint, new LinkedList<>()); - endpoints.get(endpoint).addAll(globallyDisabledEndpoints.get(endpoint)); - } - } - return endpoints; - } - - private String checkRoleBasedAccessPermissions(RestRequest request, Endpoint endpoint) { - final boolean isTraceEnabled = logger.isTraceEnabled(); - if (isTraceEnabled) { - logger.trace("Checking role based admin access for endpoint {} and method {}", endpoint.name(), request.method().name()); - } - final boolean isDebugEnabled = logger.isDebugEnabled(); - // Role based access. Check that user has role suitable for admin access - // and that the role has also access to this endpoint. - if (this.roleBasedAccessEnabled) { - - // get current user and roles - final Pair userAndRemoteAddress = - Utils.userAndRemoteAddressFrom(threadPool.getThreadContext()); - final User user = userAndRemoteAddress.getLeft(); - final TransportAddress remoteAddress = userAndRemoteAddress.getRight(); - - // map the users Security roles - Set userRoles = privilegesEvaluator.mapRoles(user, remoteAddress); - - // check if user has any role that grants access - if (currentUserHasRestApiAccess(userRoles)) { - // yes, calculate disabled end points. Since a user can have - // multiple roles, the endpoint - // needs to be disabled in all roles. - Map> disabledEndpointsForUser = getDisabledEndpointsForCurrentUser(user.getName(), userRoles); - - if (isDebugEnabled) { - logger.debug("Disabled endpoints for user {} : {} ", user, disabledEndpointsForUser); - } - - // check if we have any disabled methods for this endpoint - List disabledMethodsForEndpoint = disabledEndpointsForUser.get(endpoint); - - // no settings, all methods for this endpoint allowed - if (disabledMethodsForEndpoint == null || disabledMethodsForEndpoint.isEmpty()) { - if (isDebugEnabled) { - logger.debug("No disabled methods for user {} and endpoint {}, access allowed ", user, endpoint); - } - return null; - } - - // some methods disabled, check requested method - if (!disabledMethodsForEndpoint.contains(request.method())) { - if (isDebugEnabled) { - logger.debug("Request method {} for user {} and endpoint {} not restricted, access allowed ", request.method(), user, endpoint); - } - return null; - } - - logger.info("User {} with Security roles {} does not have access to endpoint {} and method {}, checking admin TLS certificate now.", user, userRoles, - endpoint.name(), request.method()); - return "User " + user.getName() + " with Security roles " + userRoles + " does not have any access to endpoint " + endpoint.name() + " and method " - + request.method().name(); - } else { - // no, but maybe the request contains a client certificate. - // Remember error reason for better response message later on. - logger.info("User {} with Security roles {} does not have any role privileged for admin access.", user, userRoles); - return "User " + user.getName() + " with Security roles " + userRoles + " does not have any role privileged for admin access"; - } - } - return "Role based access not enabled."; - } - - private String checkAdminCertBasedAccessPermissions(RestRequest request) throws IOException { - if (logger.isTraceEnabled()) { - logger.trace("Checking certificate based admin access for path {} and method {}", request.path(), request.method().name()); - } - - // Certificate based access, Check if we have an admin TLS certificate - SSLRequestHelper.SSLInfo sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, request, principalExtractor); - - if (sslInfo == null) { - // here we log on error level, since authentication finally failed - logger.warn("No ssl info found in request."); - return "No ssl info found in request."; - } - - X509Certificate[] certs = sslInfo.getX509Certs(); - - if (certs == null || certs.length == 0) { - logger.warn("No client TLS certificate found in request"); - return "No client TLS certificate found in request"; - } - - if (!adminDNs.isAdminDN(sslInfo.getPrincipal())) { - logger.warn("Security admin permissions required but {} is not an admin", sslInfo.getPrincipal()); - return "Security admin permissions required but " + sslInfo.getPrincipal() + " is not an admin"; - } - return null; - } - - private String constructAccessErrorMessage(String roleBasedAccessFailure, String certBasedAccessFailure) { - return roleBasedAccessFailure + ". " + certBasedAccessFailure; - } + protected final Logger logger = LogManager.getLogger(this.getClass()); + + private final AdminDNs adminDNs; + private final PrivilegesEvaluator privilegesEvaluator; + private final PrincipalExtractor principalExtractor; + private final Path configPath; + private final ThreadPool threadPool; + private final Settings settings; + + private final Set allowedRoles = new HashSet<>(); + + // endpoints per role, read and cached from settings. Changes here require a + // node restart, so it's save to cache. + private final Map>> disabledEndpointsForRoles = new HashMap<>(); + + // endpoints per user, evaluated and cached dynamically. Changes here + // require a node restart, so it's save to cache. + private final Map>> disabledEndpointsForUsers = new HashMap<>(); + + // globally disabled endpoints and methods, will always be forbidden + Map> globallyDisabledEndpoints = new HashMap<>(); + + // all endpoints and methods, will be returned for users that do not have any access at all + Map> allEndpoints = new HashMap<>(); + + private final Boolean roleBasedAccessEnabled; + + public RestApiPrivilegesEvaluator( + final Settings settings, + final AdminDNs adminDNs, + final PrivilegesEvaluator privilegesEvaluator, + final PrincipalExtractor principalExtractor, + final Path configPath, + ThreadPool threadPool + ) { + + this.adminDNs = adminDNs; + this.privilegesEvaluator = privilegesEvaluator; + this.principalExtractor = principalExtractor; + this.configPath = configPath; + this.threadPool = threadPool; + this.settings = settings; + // set up + // all endpoints and methods + Map> allEndpoints = new HashMap<>(); + for (Endpoint endpoint : Endpoint.values()) { + List allMethods = new LinkedList<>(); + allMethods.addAll(Arrays.asList(Method.values())); + allEndpoints.put(endpoint, allMethods); + } + this.allEndpoints = Collections.unmodifiableMap(allEndpoints); + + // setup role based permissions + allowedRoles.addAll(settings.getAsList(ConfigConstants.SECURITY_RESTAPI_ROLES_ENABLED)); + + this.roleBasedAccessEnabled = !allowedRoles.isEmpty(); + + // globally disabled endpoints, disables access to Endpoint/Method combination for all roles + Settings globalSettings = settings.getAsSettings(ConfigConstants.SECURITY_RESTAPI_ENDPOINTS_DISABLED + ".global"); + if (!globalSettings.isEmpty()) { + globallyDisabledEndpoints = parseDisabledEndpoints(globalSettings); + } + + final boolean isDebugEnabled = logger.isDebugEnabled(); + if (isDebugEnabled) { + logger.debug("Globally disabled endpoints: {}", globallyDisabledEndpoints); + } + + for (String role : allowedRoles) { + Settings settingsForRole = settings.getAsSettings(ConfigConstants.SECURITY_RESTAPI_ENDPOINTS_DISABLED + "." + role); + if (settingsForRole.isEmpty()) { + if (isDebugEnabled) { + logger.debug("No disabled endpoints/methods for permitted role {} found, allowing all", role); + } + continue; + } + Map> disabledEndpointsForRole = parseDisabledEndpoints(settingsForRole); + if (!disabledEndpointsForRole.isEmpty()) { + disabledEndpointsForRoles.put(role, disabledEndpointsForRole); + } else { + logger.warn("Disabled endpoints/methods empty for role {}, please check configuration", role); + } + } + if (logger.isTraceEnabled()) { + logger.trace("Parsed permission set for endpoints: {}", disabledEndpointsForRoles); + } + } + + @SuppressWarnings({ "rawtypes" }) + private Map> parseDisabledEndpoints(Settings settings) { + + // Expects Setting like: 'ACTIONGROUPS=["GET", "POST"]' + if (settings == null || settings.isEmpty()) { + logger.error("Settings for disabled endpoint is null or empty: '{}', skipping.", settings); + return Collections.emptyMap(); + } + + final Map> disabledEndpoints = new HashMap>(); + + Map disabledEndpointsSettings = Utils.convertJsonToxToStructuredMap(settings); + + for (Entry value : disabledEndpointsSettings.entrySet()) { + // key is the endpoint, see if it is a valid one + String endpointString = value.getKey().toUpperCase(); + Endpoint endpoint = null; + try { + endpoint = Endpoint.valueOf(endpointString); + } catch (Exception e) { + logger.error("Unknown endpoint '{}' found in configuration, skipping.", endpointString); + continue; + } + // value must be non null + if (value.getValue() == null) { + logger.error("Disabled HTTP methods of endpoint '{}' is null, skipping.", endpointString); + continue; + } + + // value must be an array of methods + if (!(value.getValue() instanceof Collection)) { + logger.error( + "Disabled HTTP methods of endpoint '{}' must be an array, actually is '{}', skipping.", + endpointString, + (value.getValue().toString()) + ); + } + List disabledMethods = new LinkedList<>(); + for (Object disabledMethodObj : (Collection) value.getValue()) { + if (disabledMethodObj == null) { + logger.error("Found null value in disabled HTTP methods of endpoint '{}', skipping.", endpointString); + continue; + } + + if (!(disabledMethodObj instanceof String)) { + logger.error("Found non-String value in disabled HTTP methods of endpoint '{}', skipping.", endpointString); + continue; + } + + String disabledMethodAsString = (String) disabledMethodObj; + + // Provide support for '*', means all methods + if (disabledMethodAsString.trim().equals("*")) { + disabledMethods.addAll(Arrays.asList(Method.values())); + break; + } + // no wild card, disabled method must be one of + // RestRequest.Method + Method disabledMethod = null; + try { + disabledMethod = Method.valueOf(disabledMethodAsString.toUpperCase()); + } catch (Exception e) { + logger.error( + "Invalid HTTP method '{}' found in disabled HTTP methods of endpoint '{}', skipping.", + disabledMethodAsString.toUpperCase(), + endpointString + ); + continue; + } + disabledMethods.add(disabledMethod); + } + + disabledEndpoints.put(endpoint, disabledMethods); + + } + return disabledEndpoints; + } + + /** + * Check if the current request is allowed to use the REST API and the + * requested end point. Using an admin certificate grants all permissions. A + * user/role can have restricted end points. + * + * @return an error message if user does not have access, null otherwise + * TODO: log failed attempt in audit log + */ + public String checkAccessPermissions(RestRequest request, Endpoint endpoint) throws IOException { + + if (logger.isDebugEnabled()) { + logger.debug( + "Checking admin access for endpoint {}, path {} and method {}", + endpoint.name(), + request.path(), + request.method().name() + ); + } + + // Grant permission for Account endpoint. + // Return null to grant access. + if (endpoint == Endpoint.ACCOUNT) { + return null; + } + + String roleBasedAccessFailureReason = checkRoleBasedAccessPermissions(request, endpoint); + // Role based access granted + if (roleBasedAccessFailureReason == null) { + return null; + } + + String certBasedAccessFailureReason = checkAdminCertBasedAccessPermissions(request); + // TLS access granted, skip checking roles + if (certBasedAccessFailureReason == null) { + return null; + } + + return constructAccessErrorMessage(roleBasedAccessFailureReason, certBasedAccessFailureReason); + } + + public Boolean currentUserHasRestApiAccess(Set userRoles) { + + // check if user has any role that grants access + return !Collections.disjoint(allowedRoles, userRoles); + + } + + public Map> getDisabledEndpointsForCurrentUser(String userPrincipal, Set userRoles) { + + final boolean isDebugEnabled = logger.isDebugEnabled(); + + // cache + if (disabledEndpointsForUsers.containsKey(userPrincipal)) { + return disabledEndpointsForUsers.get(userPrincipal); + } + + if (!currentUserHasRestApiAccess(userRoles)) { + return this.allEndpoints; + } + + // will contain the final list of disabled endpoints and methods + Map> finalEndpoints = new HashMap<>(); + + // List of all disabled endpoints for user. Disabled endpoints must be configured in all + // roles to take effect. If a role contains a disabled endpoint, but another role + // allows this endpoint (i.e. not contained in the disabled endpoints for this role), + // the access is allowed. + + // make list mutable + List remainingEndpoints = new LinkedList<>(Arrays.asList(Endpoint.values())); + + // only retain endpoints contained in all roles for user + boolean hasDisabledEndpoints = false; + for (String userRole : userRoles) { + Map> endpointsForRole = disabledEndpointsForRoles.get(userRole); + if (endpointsForRole == null || endpointsForRole.isEmpty()) { + continue; + } + Set disabledEndpoints = endpointsForRole.keySet(); + remainingEndpoints.retainAll(disabledEndpoints); + hasDisabledEndpoints = true; + } + + if (isDebugEnabled) { + logger.debug("Remaining endpoints for user {} after retaining all : {}", userPrincipal, remainingEndpoints); + } + + // if user does not have any disabled endpoints, only globally disabled endpoints apply + if (!hasDisabledEndpoints) { + + if (isDebugEnabled) { + logger.debug( + "No disabled endpoints for user {} at all, only globally disabledendpoints apply.", + userPrincipal, + remainingEndpoints + ); + } + disabledEndpointsForUsers.put(userPrincipal, addGloballyDisabledEndpoints(finalEndpoints)); + return finalEndpoints; + + } + + // one or more disabled remaining endpoints, keep only + // methods contained in all roles for each endpoint + for (Endpoint endpoint : remainingEndpoints) { + // make list mutable + List remainingMethodsForEndpoint = new LinkedList<>(Arrays.asList(Method.values())); + for (String userRole : userRoles) { + Map> endpoints = disabledEndpointsForRoles.get(userRole); + if (endpoints != null && !endpoints.isEmpty()) { + remainingMethodsForEndpoint.retainAll(endpoints.get(endpoint)); + } + } + + finalEndpoints.put(endpoint, remainingMethodsForEndpoint); + } + + if (isDebugEnabled) { + logger.debug("Disabled endpoints for user {} after retaining all : {}", userPrincipal, finalEndpoints); + } + + // add globally disabled endpoints and methods, will always be disabled + addGloballyDisabledEndpoints(finalEndpoints); + disabledEndpointsForUsers.put(userPrincipal, finalEndpoints); + + if (isDebugEnabled) { + logger.debug( + "Disabled endpoints for user {} after retaining all : {}", + userPrincipal, + disabledEndpointsForUsers.get(userPrincipal) + ); + } + + return disabledEndpointsForUsers.get(userPrincipal); + } + + private Map> addGloballyDisabledEndpoints(Map> endpoints) { + if (globallyDisabledEndpoints != null && !globallyDisabledEndpoints.isEmpty()) { + Set globalEndoints = globallyDisabledEndpoints.keySet(); + for (Endpoint endpoint : globalEndoints) { + endpoints.putIfAbsent(endpoint, new LinkedList<>()); + endpoints.get(endpoint).addAll(globallyDisabledEndpoints.get(endpoint)); + } + } + return endpoints; + } + + private String checkRoleBasedAccessPermissions(RestRequest request, Endpoint endpoint) { + final boolean isTraceEnabled = logger.isTraceEnabled(); + if (isTraceEnabled) { + logger.trace("Checking role based admin access for endpoint {} and method {}", endpoint.name(), request.method().name()); + } + final boolean isDebugEnabled = logger.isDebugEnabled(); + // Role based access. Check that user has role suitable for admin access + // and that the role has also access to this endpoint. + if (this.roleBasedAccessEnabled) { + + // get current user and roles + final Pair userAndRemoteAddress = Utils.userAndRemoteAddressFrom(threadPool.getThreadContext()); + final User user = userAndRemoteAddress.getLeft(); + final TransportAddress remoteAddress = userAndRemoteAddress.getRight(); + + // map the users Security roles + Set userRoles = privilegesEvaluator.mapRoles(user, remoteAddress); + + // check if user has any role that grants access + if (currentUserHasRestApiAccess(userRoles)) { + // yes, calculate disabled end points. Since a user can have + // multiple roles, the endpoint + // needs to be disabled in all roles. + Map> disabledEndpointsForUser = getDisabledEndpointsForCurrentUser(user.getName(), userRoles); + + if (isDebugEnabled) { + logger.debug("Disabled endpoints for user {} : {} ", user, disabledEndpointsForUser); + } + + // check if we have any disabled methods for this endpoint + List disabledMethodsForEndpoint = disabledEndpointsForUser.get(endpoint); + + // no settings, all methods for this endpoint allowed + if (disabledMethodsForEndpoint == null || disabledMethodsForEndpoint.isEmpty()) { + if (isDebugEnabled) { + logger.debug("No disabled methods for user {} and endpoint {}, access allowed ", user, endpoint); + } + return null; + } + + // some methods disabled, check requested method + if (!disabledMethodsForEndpoint.contains(request.method())) { + if (isDebugEnabled) { + logger.debug( + "Request method {} for user {} and endpoint {} not restricted, access allowed ", + request.method(), + user, + endpoint + ); + } + return null; + } + + logger.info( + "User {} with Security roles {} does not have access to endpoint {} and method {}, checking admin TLS certificate now.", + user, + userRoles, + endpoint.name(), + request.method() + ); + return "User " + + user.getName() + + " with Security roles " + + userRoles + + " does not have any access to endpoint " + + endpoint.name() + + " and method " + + request.method().name(); + } else { + // no, but maybe the request contains a client certificate. + // Remember error reason for better response message later on. + logger.info("User {} with Security roles {} does not have any role privileged for admin access.", user, userRoles); + return "User " + + user.getName() + + " with Security roles " + + userRoles + + " does not have any role privileged for admin access"; + } + } + return "Role based access not enabled."; + } + + private String checkAdminCertBasedAccessPermissions(RestRequest request) throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("Checking certificate based admin access for path {} and method {}", request.path(), request.method().name()); + } + + // Certificate based access, Check if we have an admin TLS certificate + SSLRequestHelper.SSLInfo sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, request, principalExtractor); + + if (sslInfo == null) { + // here we log on error level, since authentication finally failed + logger.warn("No ssl info found in request."); + return "No ssl info found in request."; + } + + X509Certificate[] certs = sslInfo.getX509Certs(); + + if (certs == null || certs.length == 0) { + logger.warn("No client TLS certificate found in request"); + return "No client TLS certificate found in request"; + } + + if (!adminDNs.isAdminDN(sslInfo.getPrincipal())) { + logger.warn("Security admin permissions required but {} is not an admin", sslInfo.getPrincipal()); + return "Security admin permissions required but " + sslInfo.getPrincipal() + " is not an admin"; + } + return null; + } + + private String constructAccessErrorMessage(String roleBasedAccessFailure, String certBasedAccessFailure) { + return roleBasedAccessFailure + ". " + certBasedAccessFailure; + } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java index 7b2676c246..65dbaac05e 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesApiAction.java @@ -39,62 +39,79 @@ import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class RolesApiAction extends PatchableResourceApiAction { - private static final List routes = addRoutesPrefix(ImmutableList.of( - new Route(Method.GET, "/roles/"), - new Route(Method.GET, "/roles/{name}"), - new Route(Method.DELETE, "/roles/{name}"), - new Route(Method.PUT, "/roles/{name}"), - new Route(Method.PATCH, "/roles/"), - new Route(Method.PATCH, "/roles/{name}") - )); + private static final List routes = addRoutesPrefix( + ImmutableList.of( + new Route(Method.GET, "/roles/"), + new Route(Method.GET, "/roles/{name}"), + new Route(Method.DELETE, "/roles/{name}"), + new Route(Method.PUT, "/roles/{name}"), + new Route(Method.PATCH, "/roles/"), + new Route(Method.PATCH, "/roles/{name}") + ) + ); - @Inject - public RolesApiAction(Settings settings, final Path configPath, RestController controller, Client client, AdminDNs adminDNs, ConfigurationRepository cl, - ClusterService cs, final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); - } + @Inject + public RolesApiAction( + Settings settings, + final Path configPath, + RestController controller, + Client client, + AdminDNs adminDNs, + ConfigurationRepository cl, + ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { + super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + } - @Override - public List routes() { - return routes; - } + @Override + public List routes() { + return routes; + } - @Override - protected Endpoint getEndpoint() { - return Endpoint.ROLES; - } + @Override + protected Endpoint getEndpoint() { + return Endpoint.ROLES; + } - @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new RolesValidator(request, isSuperAdmin(), ref, this.settings, param); - } + @Override + protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { + return new RolesValidator(request, isSuperAdmin(), ref, this.settings, param); + } - @Override - protected String getResourceName() { - return "role"; - } + @Override + protected String getResourceName() { + return "role"; + } - @Override + @Override protected CType getConfigName() { return CType.ROLES; - } + } - @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfiguration, final Object content, final String resourceName) throws IOException { - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(content)) { - return isSuperAdmin(); - } else { - return true; - } - } + @Override + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfiguration, + final Object content, + final String resourceName + ) throws IOException { + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(content)) { + return isSuperAdmin(); + } else { + return true; + } + } - @Override - protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(existingConfiguration.getCEntry(name))) { - return !isSuperAdmin(); - } else { - return super.isReadOnly(existingConfiguration, name); - } - } + @Override + protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(existingConfiguration.getCEntry(name))) { + return !isSuperAdmin(); + } else { + return super.isReadOnly(existingConfiguration, name); + } + } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java index 72928cd0ad..627dabfaa2 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/RolesMappingApiAction.java @@ -43,102 +43,125 @@ import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class RolesMappingApiAction extends PatchableResourceApiAction { - private static final List routes = addRoutesPrefix(ImmutableList.of( - new Route(Method.GET, "/rolesmapping/"), - new Route(Method.GET, "/rolesmapping/{name}"), - new Route(Method.DELETE, "/rolesmapping/{name}"), - new Route(Method.PUT, "/rolesmapping/{name}"), - new Route(Method.PATCH, "/rolesmapping/"), - new Route(Method.PATCH, "/rolesmapping/{name}") - )); - - @Inject - public RolesMappingApiAction(final Settings settings, final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDNs, final ConfigurationRepository cl, final ClusterService cs, - final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { - super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); - } - - @Override - protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { - final String name = request.param("name"); - - if (name == null || name.length() == 0) { - badRequestResponse(channel, "No " + getResourceName() + " specified."); - return; - } - - final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); - final SecurityDynamicConfiguration rolesMappingConfiguration = load(getConfigName(), false); - final boolean rolesMappingExists = rolesMappingConfiguration.exists(name); - - if (!isValidRolesMapping(channel, name)) return; - - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(name))) { - if (!isSuperAdmin()) { - forbidden(channel, "No permissions"); - return; - } - } - rolesMappingConfiguration.putCObject(name, DefaultObjectMapper.readTree(content, rolesMappingConfiguration.getImplementingClass())); - - saveAndUpdateConfigs(this.securityIndexName,client, getConfigName(), rolesMappingConfiguration, new OnSucessActionListener(channel) { - - @Override - public void onResponse(IndexResponse response) { - if (rolesMappingExists) { - successResponse(channel, "'" + name + "' updated."); - } else { - createdResponse(channel, "'" + name + "' created."); - } - - } - }); - } - - @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, final Object content, final String resourceName) throws IOException { - final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(resourceName))) { - return isSuperAdmin(); - } else { - return true; - } - } - - @Override - protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { - final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); - if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(name))) { - return !isSuperAdmin(); - } else { - return super.isReadOnly(existingConfiguration, name); - } - } - - @Override - public List routes() { - return routes; - } - - @Override - protected Endpoint getEndpoint() { - return Endpoint.ROLESMAPPING; - } - - @Override - protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { - return new RolesMappingValidator(request, isSuperAdmin(), ref, this.settings, param); - } - - @Override - protected String getResourceName() { - return "rolesmapping"; - } - - @Override + private static final List routes = addRoutesPrefix( + ImmutableList.of( + new Route(Method.GET, "/rolesmapping/"), + new Route(Method.GET, "/rolesmapping/{name}"), + new Route(Method.DELETE, "/rolesmapping/{name}"), + new Route(Method.PUT, "/rolesmapping/{name}"), + new Route(Method.PATCH, "/rolesmapping/"), + new Route(Method.PATCH, "/rolesmapping/{name}") + ) + ); + + @Inject + public RolesMappingApiAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { + super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); + } + + @Override + protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { + final String name = request.param("name"); + + if (name == null || name.length() == 0) { + badRequestResponse(channel, "No " + getResourceName() + " specified."); + return; + } + + final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); + final SecurityDynamicConfiguration rolesMappingConfiguration = load(getConfigName(), false); + final boolean rolesMappingExists = rolesMappingConfiguration.exists(name); + + if (!isValidRolesMapping(channel, name)) return; + + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(name))) { + if (!isSuperAdmin()) { + forbidden(channel, "No permissions"); + return; + } + } + rolesMappingConfiguration.putCObject(name, DefaultObjectMapper.readTree(content, rolesMappingConfiguration.getImplementingClass())); + + saveAndUpdateConfigs( + this.securityIndexName, + client, + getConfigName(), + rolesMappingConfiguration, + new OnSucessActionListener(channel) { + + @Override + public void onResponse(IndexResponse response) { + if (rolesMappingExists) { + successResponse(channel, "'" + name + "' updated."); + } else { + createdResponse(channel, "'" + name + "' created."); + } + + } + } + ); + } + + @Override + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) throws IOException { + final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(resourceName))) { + return isSuperAdmin(); + } else { + return true; + } + } + + @Override + protected boolean isReadOnly(SecurityDynamicConfiguration existingConfiguration, String name) { + final SecurityDynamicConfiguration rolesConfiguration = load(CType.ROLES, false); + if (restApiAdminPrivilegesEvaluator.containsRestApiAdminPermissions(rolesConfiguration.getCEntry(name))) { + return !isSuperAdmin(); + } else { + return super.isReadOnly(existingConfiguration, name); + } + } + + @Override + public List routes() { + return routes; + } + + @Override + protected Endpoint getEndpoint() { + return Endpoint.ROLESMAPPING; + } + + @Override + protected AbstractConfigurationValidator getValidator(RestRequest request, BytesReference ref, Object... param) { + return new RolesMappingValidator(request, isSuperAdmin(), ref, this.settings, param); + } + + @Override + protected String getResourceName() { + return "rolesmapping"; + } + + @Override protected CType getConfigName() { return CType.ROLESMAPPING; - } + } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigAction.java index 66888bc126..9584a34c06 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityConfigAction.java @@ -44,26 +44,30 @@ public class SecurityConfigAction extends PatchableResourceApiAction { - private static final List getRoutes = addRoutesPrefix(Collections.singletonList( - new Route(Method.GET, "/securityconfig/") - )); - - private static final List allRoutes = new ImmutableList.Builder() - .addAll(getRoutes) - .addAll(addRoutesPrefix( - ImmutableList.of( - new Route(Method.PUT, "/securityconfig/{name}"), - new Route(Method.PATCH, "/securityconfig/") - ) - )) - .build(); + private static final List getRoutes = addRoutesPrefix(Collections.singletonList(new Route(Method.GET, "/securityconfig/"))); + + private static final List allRoutes = new ImmutableList.Builder().addAll(getRoutes) + .addAll( + addRoutesPrefix(ImmutableList.of(new Route(Method.PUT, "/securityconfig/{name}"), new Route(Method.PATCH, "/securityconfig/"))) + ) + .build(); private final boolean allowPutOrPatch; @Inject - public SecurityConfigAction(final Settings settings, final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDNs, final ConfigurationRepository cl, final ClusterService cs, - final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { + public SecurityConfigAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); allowPutOrPatch = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_RESTAPI_ALLOW_SECURITYCONFIG_MODIFICATION, false); @@ -75,14 +79,16 @@ public List routes() { } @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName) { + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) { return true; } @Override - protected void handleGet(RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException{ + protected void handleGet(RestChannel channel, RestRequest request, Client client, final JsonNode content) throws IOException { final SecurityDynamicConfiguration configuration = load(getConfigName(), true); filter(configuration); @@ -90,8 +96,6 @@ protected void handleGet(RestChannel channel, RestRequest request, Client client successResponse(channel, configuration); } - - @Override protected void handleApiRequest(RestChannel channel, RestRequest request, Client client) throws IOException { if (request.method() == Method.PATCH && !allowPutOrPatch) { @@ -102,10 +106,11 @@ protected void handleApiRequest(RestChannel channel, RestRequest request, Client } @Override - protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException{ + protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { if (allowPutOrPatch) { - if(!"config".equals(request.param("name"))) { + if (!"config".equals(request.param("name"))) { badRequestResponse(channel, "name must be config"); return; } @@ -117,12 +122,14 @@ protected void handlePut(RestChannel channel, final RestRequest request, final C } @Override - protected void handlePost(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException{ + protected void handlePost(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { notImplemented(channel, Method.POST); } @Override - protected void handleDelete(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException{ + protected void handleDelete(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { notImplemented(channel, Method.DELETE); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java index cd489cab4d..e7d68a8677 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecurityRestApiActions.java @@ -33,39 +33,296 @@ public class SecurityRestApiActions { - public static Collection getHandler(final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDns, - final ConfigurationRepository cr, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, - final ThreadPool threadPool, - final AuditLog auditLog, - final SecurityKeyStore securityKeyStore, - final UserService userService, - final boolean certificatesReloadEnabled) { + public static Collection getHandler( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDns, + final ConfigurationRepository cr, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + final ThreadPool threadPool, + final AuditLog auditLog, + final SecurityKeyStore securityKeyStore, + final UserService userService, + final boolean certificatesReloadEnabled + ) { final List handlers = new ArrayList(16); - handlers.add(new InternalUsersApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, userService, auditLog)); - handlers.add(new RolesMappingApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new RolesApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new ActionGroupsApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new FlushCacheApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new SecurityConfigAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new PermissionsInfoAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new AuthTokenProcessorAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new TenantsApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new MigrateApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new ValidateApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new AccountApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new NodesDnApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new WhitelistApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new AllowlistApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new AuditApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new MultiTenancyConfigApiAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog)); - handlers.add(new SecuritySSLCertsAction(settings, configPath, controller, client, adminDns, cr, cs, principalExtractor, evaluator, threadPool, auditLog, securityKeyStore, certificatesReloadEnabled)); + handlers.add( + new InternalUsersApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + userService, + auditLog + ) + ); + handlers.add( + new RolesMappingApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new RolesApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new ActionGroupsApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new FlushCacheApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new SecurityConfigAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new PermissionsInfoAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new AuthTokenProcessorAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new TenantsApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new MigrateApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new ValidateApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new AccountApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new NodesDnApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new WhitelistApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new AllowlistApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new AuditApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new MultiTenancyConfigApiAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog + ) + ); + handlers.add( + new SecuritySSLCertsAction( + settings, + configPath, + controller, + client, + adminDns, + cr, + cs, + principalExtractor, + evaluator, + threadPool, + auditLog, + securityKeyStore, + certificatesReloadEnabled + ) + ); return Collections.unmodifiableCollection(handlers); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java index 1c1fe9b815..4949dedad9 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/SecuritySSLCertsAction.java @@ -51,7 +51,6 @@ import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; - /** * Rest API action to get SSL certificate information related to http and transport encryption. * Only super admin users are allowed to access this API. @@ -60,10 +59,7 @@ */ public class SecuritySSLCertsAction extends AbstractApiAction { private static final List ROUTES = addRoutesPrefix( - ImmutableList.of( - new Route(Method.GET, "/ssl/certs"), - new Route(Method.PUT, "/ssl/{certType}/reloadcerts") - ) + ImmutableList.of(new Route(Method.GET, "/ssl/certs"), new Route(Method.PUT, "/ssl/{certType}/reloadcerts")) ); private final Logger log = LogManager.getLogger(this.getClass()); @@ -74,19 +70,21 @@ public class SecuritySSLCertsAction extends AbstractApiAction { private final boolean httpsEnabled; - public SecuritySSLCertsAction(final Settings settings, - final Path configPath, - final RestController controller, - final Client client, - final AdminDNs adminDNs, - final ConfigurationRepository cl, - final ClusterService cs, - final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator privilegesEvaluator, - final ThreadPool threadPool, - final AuditLog auditLog, - final SecurityKeyStore securityKeyStore, - final boolean certificatesReloadEnabled) { + public SecuritySSLCertsAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator privilegesEvaluator, + final ThreadPool threadPool, + final AuditLog auditLog, + final SecurityKeyStore securityKeyStore, + final boolean certificatesReloadEnabled + ) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, privilegesEvaluator, threadPool, auditLog); this.securityKeyStore = securityKeyStore; this.certificatesReloadEnabled = certificatesReloadEnabled; @@ -94,9 +92,11 @@ public SecuritySSLCertsAction(final Settings settings, } @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName) { + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) { return true; } @@ -122,13 +122,13 @@ protected void handleApiRequest(final RestChannel channel, final RestRequest req } if (!certificatesReloadEnabled) { badRequestResponse( - channel, - String.format( - "no handler found for uri [%s] and method [%s]. In order to use SSL reload functionality set %s to true", - request.path(), - request.method(), - ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED - ) + channel, + String.format( + "no handler found for uri [%s] and method [%s]. In order to use SSL reload functionality set %s to true", + request.path(), + request.method(), + ConfigConstants.SECURITY_SSL_CERT_RELOAD_ENABLED + ) ); return; } @@ -172,28 +172,21 @@ protected void handleApiRequest(final RestChannel channel, final RestRequest req * @throws IOException */ @Override - protected void handleGet(final RestChannel channel, - final RestRequest request, - final Client client, - final JsonNode content) throws IOException { + protected void handleGet(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { if (securityKeyStore == null) { noKeyStoreResponse(channel); return; } try (final XContentBuilder contentBuilder = channel.newBuilder()) { channel.sendResponse( - new BytesRestResponse( - RestStatus.OK, - contentBuilder - .startObject() - .field( - "http_certificates_list", - httpsEnabled ? generateCertDetailList(securityKeyStore.getHttpCerts()) : null - ).field( - "transport_certificates_list", - generateCertDetailList(securityKeyStore.getTransportCerts()) - ).endObject() - ) + new BytesRestResponse( + RestStatus.OK, + contentBuilder.startObject() + .field("http_certificates_list", httpsEnabled ? generateCertDetailList(securityKeyStore.getHttpCerts()) : null) + .field("transport_certificates_list", generateCertDetailList(securityKeyStore.getTransportCerts())) + .endObject() + ) ); } catch (final Exception e) { internalErrorResponse(channel, e.getMessage()); @@ -219,10 +212,8 @@ protected void handleGet(final RestChannel channel, * @throws IOException */ @Override - protected void handlePut(final RestChannel channel, - final RestRequest request, - final Client client, - final JsonNode content) throws IOException { + protected void handlePut(final RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { if (securityKeyStore == null) { noKeyStoreResponse(channel); return; @@ -237,44 +228,37 @@ protected void handlePut(final RestChannel channel, } securityKeyStore.initHttpSSLConfig(); channel.sendResponse( - new BytesRestResponse( - RestStatus.OK, - contentBuilder - .startObject() - .field("message", "updated http certs") - .endObject() - ) + new BytesRestResponse( + RestStatus.OK, + contentBuilder.startObject().field("message", "updated http certs").endObject() + ) ); break; case "transport": securityKeyStore.initTransportSSLConfig(); channel.sendResponse( - new BytesRestResponse( - RestStatus.OK, - contentBuilder - .startObject() - .field("message", "updated transport certs") - .endObject() - ) + new BytesRestResponse( + RestStatus.OK, + contentBuilder.startObject().field("message", "updated transport certs").endObject() + ) ); break; default: - forbidden(channel, - "invalid uri path, please use /_plugins/_security/api/ssl/http/reload or " - + "/_plugins/_security/api/ssl/transport/reload" + forbidden( + channel, + "invalid uri path, please use /_plugins/_security/api/ssl/http/reload or " + + "/_plugins/_security/api/ssl/transport/reload" ); break; } } catch (final Exception e) { log.error("Reload of certificates for {} failed", certType, e); try (final XContentBuilder contentBuilder = channel.newBuilder()) { - channel.sendResponse(new BytesRestResponse( - RestStatus.INTERNAL_SERVER_ERROR, - contentBuilder - .startObject() - .field("error", e.toString()) - .endObject() - ) + channel.sendResponse( + new BytesRestResponse( + RestStatus.INTERNAL_SERVER_ERROR, + contentBuilder.startObject().field("error", e.toString()).endObject() + ) ); } } @@ -284,25 +268,27 @@ private List> generateCertDetailList(final X509Certificate[] if (certs == null) { return null; } - return Arrays - .stream(certs) - .map(cert -> { - final String issuerDn = cert != null && cert.getIssuerX500Principal() != null ? cert.getIssuerX500Principal().getName() : ""; - final String subjectDn = cert != null && cert.getSubjectX500Principal() != null ? cert.getSubjectX500Principal().getName() : ""; + return Arrays.stream(certs).map(cert -> { + final String issuerDn = cert != null && cert.getIssuerX500Principal() != null ? cert.getIssuerX500Principal().getName() : ""; + final String subjectDn = cert != null && cert.getSubjectX500Principal() != null ? cert.getSubjectX500Principal().getName() : ""; - final String san = securityKeyStore.getSubjectAlternativeNames(cert); + final String san = securityKeyStore.getSubjectAlternativeNames(cert); - final String notBefore = cert != null && cert.getNotBefore() != null ? cert.getNotBefore().toInstant().toString() : ""; - final String notAfter = cert != null && cert.getNotAfter() != null ? cert.getNotAfter().toInstant().toString() : ""; - return ImmutableMap.of( - "issuer_dn", issuerDn, - "subject_dn", subjectDn, - "san", san, - "not_before", notBefore, - "not_after", notAfter - ); - }) - .collect(Collectors.toList()); + final String notBefore = cert != null && cert.getNotBefore() != null ? cert.getNotBefore().toInstant().toString() : ""; + final String notAfter = cert != null && cert.getNotAfter() != null ? cert.getNotAfter().toInstant().toString() : ""; + return ImmutableMap.of( + "issuer_dn", + issuerDn, + "subject_dn", + subjectDn, + "san", + san, + "not_before", + notBefore, + "not_after", + notAfter + ); + }).collect(Collectors.toList()); } private void noKeyStoreResponse(final RestChannel channel) throws IOException { diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java index ef8d1d3b9f..5fbb907ecf 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/TenantsApiAction.java @@ -54,26 +54,40 @@ import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class TenantsApiAction extends PatchableResourceApiAction { - private static final List routes = addRoutesPrefix(ImmutableList.of( + private static final List routes = addRoutesPrefix( + ImmutableList.of( new Route(Method.GET, "/tenants/{name}"), new Route(Method.GET, "/tenants/"), new Route(Method.DELETE, "/tenants/{name}"), new Route(Method.PUT, "/tenants/{name}"), new Route(Method.PATCH, "/tenants/"), new Route(Method.PATCH, "/tenants/{name}") - )); + ) + ); @Inject - public TenantsApiAction(final Settings settings, final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDNs, final ConfigurationRepository cl, final ClusterService cs, - final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { + public TenantsApiAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); } @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName) { + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) { return true; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java index 8e2222cab0..de79b131b3 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/ValidateApiAction.java @@ -54,23 +54,32 @@ import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; - public class ValidateApiAction extends AbstractApiAction { - private static final List routes = addRoutesPrefix(Collections.singletonList( - new Route(Method.GET, "/validate") - )); + private static final List routes = addRoutesPrefix(Collections.singletonList(new Route(Method.GET, "/validate"))); @Inject - public ValidateApiAction(final Settings settings, final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDNs, final ConfigurationRepository cl, final ClusterService cs, final PrincipalExtractor principalExtractor, - final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { + public ValidateApiAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); } @Override - protected boolean hasPermissionsToCreate(final SecurityDynamicConfiguration dynamicConfigFactory, - final Object content, - final String resourceName) { + protected boolean hasPermissionsToCreate( + final SecurityDynamicConfiguration dynamicConfigFactory, + final Object content, + final String resourceName + ) { return true; } @@ -99,17 +108,36 @@ protected void handleGet(RestChannel channel, RestRequest request, Client client try { final SecurityDynamicConfiguration configV6 = (SecurityDynamicConfiguration) loadedConfig; - final SecurityDynamicConfiguration actionGroupsV6 = (SecurityDynamicConfiguration) load(CType.ACTIONGROUPS, true, acceptInvalid); - final SecurityDynamicConfiguration internalUsersV6 = (SecurityDynamicConfiguration) load(CType.INTERNALUSERS, true, acceptInvalid); - final SecurityDynamicConfiguration rolesV6 = (SecurityDynamicConfiguration) load(CType.ROLES, true, acceptInvalid); - final SecurityDynamicConfiguration rolesmappingV6 = (SecurityDynamicConfiguration) load(CType.ROLESMAPPING, true, acceptInvalid); - final SecurityDynamicConfiguration auditConfigV6 = (SecurityDynamicConfiguration) load(CType.AUDIT, true); + final SecurityDynamicConfiguration actionGroupsV6 = (SecurityDynamicConfiguration) load( + CType.ACTIONGROUPS, + true, + acceptInvalid + ); + final SecurityDynamicConfiguration internalUsersV6 = (SecurityDynamicConfiguration) load( + CType.INTERNALUSERS, + true, + acceptInvalid + ); + final SecurityDynamicConfiguration rolesV6 = (SecurityDynamicConfiguration) load( + CType.ROLES, + true, + acceptInvalid + ); + final SecurityDynamicConfiguration rolesmappingV6 = (SecurityDynamicConfiguration) load( + CType.ROLESMAPPING, + true, + acceptInvalid + ); + final SecurityDynamicConfiguration auditConfigV6 = (SecurityDynamicConfiguration) load( + CType.AUDIT, + true + ); final SecurityDynamicConfiguration actionGroupsV7 = Migration.migrateActionGroups(actionGroupsV6); final SecurityDynamicConfiguration configV7 = Migration.migrateConfig(configV6); final SecurityDynamicConfiguration internalUsersV7 = Migration.migrateInternalUsers(internalUsersV6); - final Tuple, SecurityDynamicConfiguration> rolesTenantsV7 = Migration.migrateRoles(rolesV6, - rolesmappingV6); + final Tuple, SecurityDynamicConfiguration> rolesTenantsV7 = Migration + .migrateRoles(rolesV6, rolesmappingV6); final SecurityDynamicConfiguration rolesmappingV7 = Migration.migrateRoleMappings(rolesmappingV6); final SecurityDynamicConfiguration auditConfigV7 = Migration.migrateAudit(auditConfigV6); @@ -120,17 +148,20 @@ protected void handleGet(RestChannel channel, RestRequest request, Client client } @Override - protected void handleDelete(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { + protected void handleDelete(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { notImplemented(channel, Method.POST); } @Override - protected void handlePost(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { + protected void handlePost(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { notImplemented(channel, Method.GET); } @Override - protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) throws IOException { + protected void handlePut(RestChannel channel, final RestRequest request, final Client client, final JsonNode content) + throws IOException { notImplemented(channel, Method.PUT); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java b/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java index 6f55a2d762..d3bc92959c 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java +++ b/src/main/java/org/opensearch/security/dlic/rest/api/WhitelistApiAction.java @@ -74,16 +74,40 @@ *

*/ public class WhitelistApiAction extends AllowlistApiAction { - private static final List routes = addDeprecatedRoutesPrefix(ImmutableList.of( - new DeprecatedRoute(RestRequest.Method.GET, "/whitelist", "[/whitelist] is a deprecated endpoint. Please use [/allowlist] instead."), - new DeprecatedRoute(RestRequest.Method.PUT, "/whitelist", "[/whitelist] is a deprecated endpoint. Please use [/allowlist] instead."), - new DeprecatedRoute(RestRequest.Method.PATCH, "/whitelist", "[/whitelist] is a deprecated endpoint. Please use [/allowlist] instead.") - )); + private static final List routes = addDeprecatedRoutesPrefix( + ImmutableList.of( + new DeprecatedRoute( + RestRequest.Method.GET, + "/whitelist", + "[/whitelist] is a deprecated endpoint. Please use [/allowlist] instead." + ), + new DeprecatedRoute( + RestRequest.Method.PUT, + "/whitelist", + "[/whitelist] is a deprecated endpoint. Please use [/allowlist] instead." + ), + new DeprecatedRoute( + RestRequest.Method.PATCH, + "/whitelist", + "[/whitelist] is a deprecated endpoint. Please use [/allowlist] instead." + ) + ) + ); @Inject - public WhitelistApiAction(final Settings settings, final Path configPath, final RestController controller, final Client client, - final AdminDNs adminDNs, final ConfigurationRepository cl, final ClusterService cs, - final PrincipalExtractor principalExtractor, final PrivilegesEvaluator evaluator, ThreadPool threadPool, AuditLog auditLog) { + public WhitelistApiAction( + final Settings settings, + final Path configPath, + final RestController controller, + final Client client, + final AdminDNs adminDNs, + final ConfigurationRepository cl, + final ClusterService cs, + final PrincipalExtractor principalExtractor, + final PrivilegesEvaluator evaluator, + ThreadPool threadPool, + AuditLog auditLog + ) { super(settings, configPath, controller, client, adminDNs, cl, cs, principalExtractor, evaluator, threadPool, auditLog); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java index aba2807846..74908dbf60 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java +++ b/src/main/java/org/opensearch/security/dlic/rest/support/Utils.java @@ -69,7 +69,10 @@ public static Map convertJsonToxToStructuredMap(ToXContent jsonC } public static Map convertJsonToxToStructuredMap(String jsonContent) { - try (XContentParser parser = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, jsonContent)) { + try ( + XContentParser parser = XContentType.JSON.xContent() + .createParser(NamedXContentRegistry.EMPTY, THROW_UNSUPPORTED_OPERATION, jsonContent) + ) { return parser.map(); } catch (IOException e1) { throw ExceptionsHelper.convertToOpenSearchException(e1); @@ -179,7 +182,8 @@ public static Map byteArrayToMutableJsonMap(byte[] jsonBytes) th return AccessController.doPrivileged(new PrivilegedExceptionAction>() { @Override public Map run() throws Exception { - return internalMapper.readValue(jsonBytes, new TypeReference>() {}); + return internalMapper.readValue(jsonBytes, new TypeReference>() { + }); } }); } catch (final PrivilegedActionException e) { @@ -215,10 +219,7 @@ public static String hash(final char[] clearTextPassword) { * @return new set of fields resource paths */ public static Set generateFieldResourcePaths(final Set fields, final String prefix) { - return fields - .stream() - .map(field -> prefix + field) - .collect(ImmutableSet.toImmutableSet()); + return fields.stream().map(field -> prefix + field).collect(ImmutableSet.toImmutableSet()); } /** @@ -227,7 +228,7 @@ public static Set generateFieldResourcePaths(final Set fields, f * @return new list of API routes prefixed with _opendistro... and _plugins... *Total number of routes is expanded as twice as the number of routes passed in */ - public static List addRoutesPrefix(List routes){ + public static List addRoutesPrefix(List routes) { return addRoutesPrefix(routes, "/_opendistro/_security/api", "/_plugins/_security/api"); } @@ -238,12 +239,10 @@ public static List addRoutesPrefix(List routes){ * @return new list of API routes prefixed with the strings listed in prefixes * Total number of routes will be expanded len(prefixes) as much comparing to the list passed in */ - public static List addRoutesPrefix(List routes, final String... prefixes){ + public static List addRoutesPrefix(List routes, final String... prefixes) { return routes.stream() - .flatMap( - r -> Arrays.stream(prefixes) - .map(p -> new Route(r.getMethod(), p + r.getPath()))) - .collect(ImmutableList.toImmutableList()); + .flatMap(r -> Arrays.stream(prefixes).map(p -> new Route(r.getMethod(), p + r.getPath()))) + .collect(ImmutableList.toImmutableList()); } /** @@ -252,7 +251,7 @@ public static List addRoutesPrefix(List routes, final String... pr * @return new list of API routes prefixed with _opendistro... and _plugins... *Total number of routes is expanded as twice as the number of routes passed in */ - public static List addDeprecatedRoutesPrefix(List deprecatedRoutes){ + public static List addDeprecatedRoutesPrefix(List deprecatedRoutes) { return addDeprecatedRoutesPrefix(deprecatedRoutes, "/_opendistro/_security/api", "/_plugins/_security/api"); } @@ -263,12 +262,10 @@ public static List addDeprecatedRoutesPrefix(List addDeprecatedRoutesPrefix(List deprecatedRoutes, final String... prefixes){ + public static List addDeprecatedRoutesPrefix(List deprecatedRoutes, final String... prefixes) { return deprecatedRoutes.stream() - .flatMap( - r -> Arrays.stream(prefixes) - .map(p -> new DeprecatedRoute(r.getMethod(), p + r.getPath(), r.getDeprecationMessage()))) - .collect(ImmutableList.toImmutableList()); + .flatMap(r -> Arrays.stream(prefixes).map(p -> new DeprecatedRoute(r.getMethod(), p + r.getPath(), r.getDeprecationMessage()))) + .collect(ImmutableList.toImmutableList()); } public static Pair userAndRemoteAddressFrom(final ThreadContext threadContext) { diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/AbstractConfigurationValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/AbstractConfigurationValidator.java index e3221de7e6..51d58d75f6 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/AbstractConfigurationValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/AbstractConfigurationValidator.java @@ -39,7 +39,6 @@ import org.opensearch.security.DefaultObjectMapper; import org.opensearch.security.support.ConfigConstants; - public abstract class AbstractConfigurationValidator { JsonFactory factory = new JsonFactory(); @@ -91,7 +90,12 @@ public abstract class AbstractConfigurationValidator { private JsonNode contentAsNode; - public AbstractConfigurationValidator(final RestRequest request, final BytesReference ref, final Settings opensearchSettings, Object... param) { + public AbstractConfigurationValidator( + final RestRequest request, + final BytesReference ref, + final Settings opensearchSettings, + Object... param + ) { this.content = ref; this.method = request.method(); this.opensearchSettings = opensearchSettings; @@ -113,19 +117,19 @@ public boolean validate() { return true; } - if(this.payloadMandatory && content.length() == 0) { + if (this.payloadMandatory && content.length() == 0) { this.errorType = ErrorType.PAYLOAD_MANDATORY; return false; } - if(!this.payloadMandatory && content.length() == 0) { + if (!this.payloadMandatory && content.length() == 0) { return true; } - if(this.payloadMandatory && content.length() > 0) { + if (this.payloadMandatory && content.length() > 0) { try { - if(DefaultObjectMapper.readTree(content.utf8ToString()).size() == 0) { + if (DefaultObjectMapper.readTree(content.utf8ToString()).size() == 0) { this.errorType = ErrorType.PAYLOAD_MANDATORY; return false; } @@ -184,7 +188,7 @@ public boolean validate() { return false; } - //null element in the values of all the possible keys with DataType as ARRAY + // null element in the values of all the possible keys with DataType as ARRAY for (Entry allowedKey : allowedKeys.entrySet()) { JsonNode value = contentAsNode.get(allowedKey.getKey()); if (value != null) { @@ -249,18 +253,21 @@ public XContentBuilder errorsAsXContent(RestChannel channel) { break; case INVALID_PASSWORD: builder.field("status", "error"); - builder.field("reason", opensearchSettings.get( + builder.field( + "reason", + opensearchSettings.get( ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, - "Password does not match minimum criteria")); + "Password does not match minimum criteria" + ) + ); break; case WEAK_PASSWORD: case SIMILAR_PASSWORD: builder.field("status", "error"); builder.field( - "reason", - opensearchSettings.get( - ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, - errorType.message)); + "reason", + opensearchSettings.get(ConfigConstants.SECURITY_RESTAPI_PASSWORD_VALIDATION_ERROR_MESSAGE, errorType.message) + ); break; case WRONG_DATATYPE: builder.field("status", "error"); @@ -295,7 +302,10 @@ private void addErrorMessage(final XContentBuilder builder, final String message } public static enum DataType { - STRING, ARRAY, OBJECT, BOOLEAN; + STRING, + ARRAY, + OBJECT, + BOOLEAN; } public static enum ErrorType { @@ -307,7 +317,8 @@ public static enum ErrorType { WRONG_DATATYPE("Wrong datatype"), BODY_NOT_PARSEABLE("Could not parse content of request."), PAYLOAD_NOT_ALLOWED("Request body not allowed for this action."), - PAYLOAD_MANDATORY("Request body required for this action."), SECURITY_NOT_INITIALIZED("Security index not initialized"), + PAYLOAD_MANDATORY("Request body required for this action."), + SECURITY_NOT_INITIALIZED("Security index not initialized"), NULL_ARRAY_ELEMENT("`null` is not allowed as json array element"); private String message; @@ -326,8 +337,8 @@ protected final boolean hasParams() { } private boolean hasNullArrayElement(JsonNode node) { - for (JsonNode element: node) { - if(element.isNull()) { + for (JsonNode element : node) { + if (element.isNull()) { if (node.isArray()) { return true; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/ActionGroupValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/ActionGroupValidator.java index ee1ab61238..a9f298fb15 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/ActionGroupValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/ActionGroupValidator.java @@ -17,15 +17,21 @@ public class ActionGroupValidator extends AbstractConfigurationValidator { - public ActionGroupValidator(final RestRequest request, boolean isSuperAdmin, BytesReference ref, final Settings opensearchSettings, Object... param) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - allowedKeys.put("allowed_actions", DataType.ARRAY); - allowedKeys.put("description", DataType.STRING); - allowedKeys.put("type", DataType.STRING); - if (isSuperAdmin) allowedKeys.put("reserved" , DataType.BOOLEAN); + public ActionGroupValidator( + final RestRequest request, + boolean isSuperAdmin, + BytesReference ref, + final Settings opensearchSettings, + Object... param + ) { + super(request, ref, opensearchSettings, param); + this.payloadMandatory = true; + allowedKeys.put("allowed_actions", DataType.ARRAY); + allowedKeys.put("description", DataType.STRING); + allowedKeys.put("type", DataType.STRING); + if (isSuperAdmin) allowedKeys.put("reserved", DataType.BOOLEAN); - mandatoryKeys.add("allowed_actions"); - } + mandatoryKeys.add("allowed_actions"); + } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/AuditValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/AuditValidator.java index 6cc2aaca1b..1bff373c0d 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/AuditValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/AuditValidator.java @@ -25,29 +25,26 @@ public class AuditValidator extends AbstractConfigurationValidator { private static final Set DISABLED_REST_CATEGORIES = ImmutableSet.of( - AuditCategory.BAD_HEADERS, - AuditCategory.SSL_EXCEPTION, - AuditCategory.AUTHENTICATED, - AuditCategory.FAILED_LOGIN, - AuditCategory.GRANTED_PRIVILEGES, - AuditCategory.MISSING_PRIVILEGES + AuditCategory.BAD_HEADERS, + AuditCategory.SSL_EXCEPTION, + AuditCategory.AUTHENTICATED, + AuditCategory.FAILED_LOGIN, + AuditCategory.GRANTED_PRIVILEGES, + AuditCategory.MISSING_PRIVILEGES ); private static final Set DISABLED_TRANSPORT_CATEGORIES = ImmutableSet.of( - AuditCategory.BAD_HEADERS, - AuditCategory.SSL_EXCEPTION, - AuditCategory.AUTHENTICATED, - AuditCategory.FAILED_LOGIN, - AuditCategory.GRANTED_PRIVILEGES, - AuditCategory.MISSING_PRIVILEGES, - AuditCategory.INDEX_EVENT, - AuditCategory.OPENDISTRO_SECURITY_INDEX_ATTEMPT + AuditCategory.BAD_HEADERS, + AuditCategory.SSL_EXCEPTION, + AuditCategory.AUTHENTICATED, + AuditCategory.FAILED_LOGIN, + AuditCategory.GRANTED_PRIVILEGES, + AuditCategory.MISSING_PRIVILEGES, + AuditCategory.INDEX_EVENT, + AuditCategory.OPENDISTRO_SECURITY_INDEX_ATTEMPT ); - public AuditValidator(final RestRequest request, - final BytesReference ref, - final Settings opensearchSettings, - final Object... param) { + public AuditValidator(final RestRequest request, final BytesReference ref, final Settings opensearchSettings, final Object... param) { super(request, ref, opensearchSettings, param); this.payloadMandatory = true; this.allowedKeys.put("enabled", DataType.BOOLEAN); @@ -62,8 +59,8 @@ public boolean validate() { } if ((request.method() == RestRequest.Method.PUT || request.method() == RestRequest.Method.PATCH) - && this.content != null - && this.content.length() > 0) { + && this.content != null + && this.content.length() > 0) { try { // try parsing to target type final AuditConfig auditConfig = DefaultObjectMapper.readTree(getContentAsNode(), AuditConfig.class); diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/CredentialsValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/CredentialsValidator.java index ff1addc11e..a0f67c97ce 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/CredentialsValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/CredentialsValidator.java @@ -29,10 +29,7 @@ public class CredentialsValidator extends AbstractConfigurationValidator { private final PasswordValidator passwordValidator; - public CredentialsValidator(final RestRequest request, - final BytesReference ref, - final Settings opensearchSettings, - Object... param) { + public CredentialsValidator(final RestRequest request, final BytesReference ref, final Settings opensearchSettings, Object... param) { super(request, ref, opensearchSettings, param); this.payloadMandatory = true; this.passwordValidator = PasswordValidator.of(opensearchSettings); @@ -50,8 +47,8 @@ public boolean validate() { return false; } if ((request.method() == RestRequest.Method.PUT || request.method() == RestRequest.Method.PATCH) - && this.content != null - && this.content.length() > 1) { + && this.content != null + && this.content.length() > 1) { try { final Map contentAsMap = XContentHelper.convertToMap(this.content, false, XContentType.JSON).v2(); final String password = (String) contentAsMap.get("password"); @@ -75,7 +72,7 @@ public boolean validate() { } } } catch (NotXContentException e) { - //this.content is not valid json/yaml + // this.content is not valid json/yaml log.error("Invalid xContent: " + e, e); return false; } diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/InternalUsersValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/InternalUsersValidator.java index 5f9828eba4..9681c47232 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/InternalUsersValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/InternalUsersValidator.java @@ -20,8 +20,13 @@ */ public class InternalUsersValidator extends CredentialsValidator { - public InternalUsersValidator(final RestRequest request, boolean isSuperAdmin, BytesReference ref, final Settings opensearchSettings, - Object... param) { + public InternalUsersValidator( + final RestRequest request, + boolean isSuperAdmin, + BytesReference ref, + final Settings opensearchSettings, + Object... param + ) { super(request, ref, opensearchSettings, param); allowedKeys.put("backend_roles", DataType.ARRAY); allowedKeys.put("attributes", DataType.OBJECT); diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/MultiTenancyConfigValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/MultiTenancyConfigValidator.java index 42870f1c13..42f86dbee5 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/MultiTenancyConfigValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/MultiTenancyConfigValidator.java @@ -20,7 +20,6 @@ public class MultiTenancyConfigValidator extends AbstractConfigurationValidator public static final String PRIVATE_TENANT_ENABLED_JSON_PROPERTY = "private_tenant_enabled"; public static final String MULTITENANCY_ENABLED_JSON_PROPERTY = "multitenancy_enabled"; - public MultiTenancyConfigValidator(RestRequest request, BytesReference ref, Settings opensearchSettings, Object... param) { super(request, ref, opensearchSettings, param); this.payloadMandatory = true; diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/NoOpValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/NoOpValidator.java index ec85c1fae8..7c64102091 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/NoOpValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/NoOpValidator.java @@ -17,8 +17,8 @@ public class NoOpValidator extends AbstractConfigurationValidator { - public NoOpValidator(final RestRequest request, BytesReference ref, final Settings opensearchSettings, Object... param) { - super(request, ref, opensearchSettings, param); - } + public NoOpValidator(final RestRequest request, BytesReference ref, final Settings opensearchSettings, Object... param) { + super(request, ref, opensearchSettings, param); + } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/PasswordValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/PasswordValidator.java index f5ae9bee49..ac521dee8a 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/PasswordValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/PasswordValidator.java @@ -46,8 +46,8 @@ public class PasswordValidator { * are similar * "user_inputs" - is a default dictionary zxcvbn creates for checking similarity */ - private final static Predicate USERNAME_SIMILARITY_CHECK = m -> - m.pattern == com.nulabinc.zxcvbn.Pattern.Dictionary && "user_inputs".equals(m.dictionaryName); + private final static Predicate USERNAME_SIMILARITY_CHECK = m -> m.pattern == com.nulabinc.zxcvbn.Pattern.Dictionary + && "user_inputs".equals(m.dictionaryName); private final Logger logger = LogManager.getLogger(this.getClass()); @@ -59,9 +59,7 @@ public class PasswordValidator { private final Zxcvbn zxcvbn; - private PasswordValidator(final int minPasswordLength, - final Pattern passwordRegexpPattern, - final ScoreStrength scoreStrength) { + private PasswordValidator(final int minPasswordLength, final Pattern passwordRegexpPattern, final ScoreStrength scoreStrength) { this.minPasswordLength = minPasswordLength; this.passwordRegexpPattern = passwordRegexpPattern; this.scoreStrength = scoreStrength; @@ -71,49 +69,47 @@ private PasswordValidator(final int minPasswordLength, public static PasswordValidator of(final Settings settings) { final String passwordRegex = settings.get(SECURITY_RESTAPI_PASSWORD_VALIDATION_REGEX, null); final ScoreStrength scoreStrength = ScoreStrength.fromConfiguration( - settings.get(SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH, ScoreStrength.STRONG.name()) - ); + settings.get(SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH, ScoreStrength.STRONG.name()) + ); final int minPasswordLength = settings.getAsInt(SECURITY_RESTAPI_PASSWORD_MIN_LENGTH, -1); return new PasswordValidator( - minPasswordLength, - !Strings.isNullOrEmpty(passwordRegex) ? Pattern.compile(String.format("^%s$", passwordRegex)) : null, - scoreStrength); + minPasswordLength, + !Strings.isNullOrEmpty(passwordRegex) ? Pattern.compile(String.format("^%s$", passwordRegex)) : null, + scoreStrength + ); } ErrorType validate(final String username, final String password) { if (minPasswordLength > 0 && password.length() < minPasswordLength) { logger.debug( - "Password is too short, the minimum required length is {}, but current length is {}", - minPasswordLength, - password.length() + "Password is too short, the minimum required length is {}, but current length is {}", + minPasswordLength, + password.length() ); return ErrorType.INVALID_PASSWORD; } if (password.length() > MAX_LENGTH) { logger.debug( - "Password is too long, the maximum required length is {}, but current length is {}", - MAX_LENGTH, - password.length() + "Password is too long, the maximum required length is {}, but current length is {}", + MAX_LENGTH, + password.length() ); return ErrorType.INVALID_PASSWORD; } - if (Objects.nonNull(passwordRegexpPattern) - && !passwordRegexpPattern.matcher(password).matches()) { + if (Objects.nonNull(passwordRegexpPattern) && !passwordRegexpPattern.matcher(password).matches()) { logger.debug("Regex does not match password"); return ErrorType.INVALID_PASSWORD; } final Strength strength = zxcvbn.measure(password, ImmutableList.of(username)); if (strength.getScore() < scoreStrength.score()) { logger.debug( - "Password is weak the required score is {}, but current is {}", - scoreStrength, - ScoreStrength.fromScore(strength.getScore()) + "Password is weak the required score is {}, but current is {}", + scoreStrength, + ScoreStrength.fromScore(strength.getScore()) ); return ErrorType.WEAK_PASSWORD; } - final boolean similar = strength.getSequence() - .stream() - .anyMatch(USERNAME_SIMILARITY_CHECK); + final boolean similar = strength.getSequence().stream().anyMatch(USERNAME_SIMILARITY_CHECK); if (similar) { logger.debug("Password is too similar to the user name {}", username); return ErrorType.SIMILAR_PASSWORD; @@ -137,12 +133,10 @@ public enum ScoreStrength { static final List CONFIGURATION_VALUES = ImmutableList.of(FAIR, STRONG, VERY_STRONG); - static final String EXPECTED_CONFIGURATION_VALUES = - new StringJoiner(",") - .add(FAIR.name().toLowerCase(Locale.ROOT)) - .add(STRONG.name().toLowerCase(Locale.ROOT)) - .add(VERY_STRONG.name().toLowerCase(Locale.ROOT)) - .toString(); + static final String EXPECTED_CONFIGURATION_VALUES = new StringJoiner(",").add(FAIR.name().toLowerCase(Locale.ROOT)) + .add(STRONG.name().toLowerCase(Locale.ROOT)) + .add(VERY_STRONG.name().toLowerCase(Locale.ROOT)) + .toString(); private ScoreStrength(final int score, final String description) { this.score = score; @@ -151,24 +145,22 @@ private ScoreStrength(final int score, final String description) { public static ScoreStrength fromScore(final int score) { for (final ScoreStrength strength : values()) { - if (strength.score == score) - return strength; + if (strength.score == score) return strength; } throw new IllegalArgumentException("Unknown score " + score); } public static ScoreStrength fromConfiguration(final String value) { for (final ScoreStrength strength : CONFIGURATION_VALUES) { - if (strength.name().equalsIgnoreCase(value)) - return strength; + if (strength.name().equalsIgnoreCase(value)) return strength; } throw new IllegalArgumentException( - String.format( - "Setting [%s] cannot be used with the configured: %s. Expected one of [%s]", - SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH, - value, - EXPECTED_CONFIGURATION_VALUES - ) + String.format( + "Setting [%s] cannot be used with the configured: %s. Expected one of [%s]", + SECURITY_RESTAPI_PASSWORD_SCORE_BASED_VALIDATION_STRENGTH, + value, + EXPECTED_CONFIGURATION_VALUES + ) ); } diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/RolesMappingValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/RolesMappingValidator.java index 0f36371176..728c2e0ca0 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/RolesMappingValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/RolesMappingValidator.java @@ -17,19 +17,25 @@ public class RolesMappingValidator extends AbstractConfigurationValidator { - public RolesMappingValidator(final RestRequest request, boolean isSuperAdmin, final BytesReference ref, final Settings opensearchSettings, Object... param) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - allowedKeys.put("backend_roles", DataType.ARRAY); - allowedKeys.put("and_backend_roles", DataType.ARRAY); - allowedKeys.put("hosts", DataType.ARRAY); - allowedKeys.put("users", DataType.ARRAY); - allowedKeys.put("description", DataType.STRING); - if (isSuperAdmin) allowedKeys.put("reserved", DataType.BOOLEAN); + public RolesMappingValidator( + final RestRequest request, + boolean isSuperAdmin, + final BytesReference ref, + final Settings opensearchSettings, + Object... param + ) { + super(request, ref, opensearchSettings, param); + this.payloadMandatory = true; + allowedKeys.put("backend_roles", DataType.ARRAY); + allowedKeys.put("and_backend_roles", DataType.ARRAY); + allowedKeys.put("hosts", DataType.ARRAY); + allowedKeys.put("users", DataType.ARRAY); + allowedKeys.put("description", DataType.STRING); + if (isSuperAdmin) allowedKeys.put("reserved", DataType.BOOLEAN); - mandatoryOrKeys.add("backend_roles"); - mandatoryOrKeys.add("and_backend_roles"); - mandatoryOrKeys.add("hosts"); - mandatoryOrKeys.add("users"); - } + mandatoryOrKeys.add("backend_roles"); + mandatoryOrKeys.add("and_backend_roles"); + mandatoryOrKeys.add("hosts"); + mandatoryOrKeys.add("users"); + } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/RolesValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/RolesValidator.java index 07708c6615..2e57730e41 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/RolesValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/RolesValidator.java @@ -24,17 +24,23 @@ public class RolesValidator extends AbstractConfigurationValidator { - private static final Salt SALT = new Salt(new byte[] {1,2,3,4,5,1,2,3,4,5,1,2,3,4,5,6}); - - public RolesValidator(final RestRequest request, boolean isSuperAdmin, final BytesReference ref, final Settings opensearchSettings, Object... param) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; + private static final Salt SALT = new Salt(new byte[] { 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 6 }); + + public RolesValidator( + final RestRequest request, + boolean isSuperAdmin, + final BytesReference ref, + final Settings opensearchSettings, + Object... param + ) { + super(request, ref, opensearchSettings, param); + this.payloadMandatory = true; allowedKeys.put("cluster_permissions", DataType.ARRAY); allowedKeys.put("tenant_permissions", DataType.ARRAY); allowedKeys.put("index_permissions", DataType.ARRAY); allowedKeys.put("description", DataType.STRING); if (isSuperAdmin) allowedKeys.put("reserved", DataType.BOOLEAN); - } + } @Override public boolean validate() { @@ -43,7 +49,7 @@ public boolean validate() { return false; } - boolean valid=true; + boolean valid = true; if (this.content != null && this.content.length() > 0) { @@ -60,8 +66,8 @@ public boolean validate() { } } - if(!valid) { - this.errorType = ErrorType.WRONG_DATATYPE; + if (!valid) { + this.errorType = ErrorType.WRONG_DATATYPE; } return valid; @@ -71,7 +77,7 @@ private boolean validateMaskedFieldSyntax(String mf) { try { new MaskedField(mf, SALT).isValid(); } catch (Exception e) { - wrongDatatypes.put("Masked field not valid: "+mf, e.getMessage()); + wrongDatatypes.put("Masked field not valid: " + mf, e.getMessage()); return false; } return true; diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/SecurityConfigValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/SecurityConfigValidator.java index 1a8db220b9..cd2ee56b4a 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/SecurityConfigValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/SecurityConfigValidator.java @@ -17,10 +17,10 @@ public class SecurityConfigValidator extends AbstractConfigurationValidator { - public SecurityConfigValidator(final RestRequest request, BytesReference ref, final Settings opensearchSettings, Object... param) { - super(request, ref, opensearchSettings, param); - this.payloadMandatory = true; - allowedKeys.put("dynamic", DataType.OBJECT); - } + public SecurityConfigValidator(final RestRequest request, BytesReference ref, final Settings opensearchSettings, Object... param) { + super(request, ref, opensearchSettings, param); + this.payloadMandatory = true; + allowedKeys.put("dynamic", DataType.OBJECT); + } } diff --git a/src/main/java/org/opensearch/security/dlic/rest/validation/TenantValidator.java b/src/main/java/org/opensearch/security/dlic/rest/validation/TenantValidator.java index 15068500e8..51e0e97264 100644 --- a/src/main/java/org/opensearch/security/dlic/rest/validation/TenantValidator.java +++ b/src/main/java/org/opensearch/security/dlic/rest/validation/TenantValidator.java @@ -33,7 +33,13 @@ public class TenantValidator extends AbstractConfigurationValidator { - public TenantValidator(final RestRequest request, boolean isSuperAdmin, BytesReference ref, final Settings opensearchSettings, Object... param) { + public TenantValidator( + final RestRequest request, + boolean isSuperAdmin, + BytesReference ref, + final Settings opensearchSettings, + Object... param + ) { super(request, ref, opensearchSettings, param); this.payloadMandatory = true; allowedKeys.put("description", DataType.STRING); diff --git a/src/main/java/org/opensearch/security/filter/SecurityFilter.java b/src/main/java/org/opensearch/security/filter/SecurityFilter.java index 4dd629c010..38675d97c5 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityFilter.java @@ -113,9 +113,18 @@ public class SecurityFilter implements ActionFilter { private final RolesInjector rolesInjector; private final UserInjector userInjector; - public SecurityFilter(final Settings settings, final PrivilegesEvaluator evalp, final AdminDNs adminDns, - DlsFlsRequestValve dlsFlsValve, AuditLog auditLog, ThreadPool threadPool, ClusterService cs, - final CompatConfig compatConfig, final IndexResolverReplacer indexResolverReplacer, final XFFResolver xffResolver) { + public SecurityFilter( + final Settings settings, + final PrivilegesEvaluator evalp, + final AdminDNs adminDns, + DlsFlsRequestValve dlsFlsValve, + AuditLog auditLog, + ThreadPool threadPool, + ClusterService cs, + final CompatConfig compatConfig, + final IndexResolverReplacer indexResolverReplacer, + final XFFResolver xffResolver + ) { this.evalp = evalp; this.adminDns = adminDns; this.dlsFlsValve = dlsFlsValve; @@ -125,7 +134,9 @@ public SecurityFilter(final Settings settings, final PrivilegesEvaluator evalp, this.compatConfig = compatConfig; this.indexResolverReplacer = indexResolverReplacer; this.xffResolver = xffResolver; - this.immutableIndicesMatcher = WildcardMatcher.from(settings.getAsList(ConfigConstants.SECURITY_COMPLIANCE_IMMUTABLE_INDICES, Collections.emptyList())); + this.immutableIndicesMatcher = WildcardMatcher.from( + settings.getAsList(ConfigConstants.SECURITY_COMPLIANCE_IMMUTABLE_INDICES, Collections.emptyList()) + ); this.rolesInjector = new RolesInjector(auditLog); this.userInjector = new UserInjector(settings, threadPool, auditLog, xffResolver); log.info("{} indices are made immutable.", immutableIndicesMatcher); @@ -142,9 +153,14 @@ public int order() { } @Override - public void apply(Task task, final String action, Request request, - ActionListener listener, ActionFilterChain chain) { - try (StoredContext ctx = threadContext.newStoredContext(true)){ + 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); } @@ -154,11 +170,16 @@ private static Set alias2Name(Set aliases) { return aliases.stream().map(a -> a.name()).collect(ImmutableSet.toImmutableSet()); } - private void apply0(Task task, final String action, Request request, - ActionListener listener, ActionFilterChain chain) { + private void apply0( + Task task, + final String action, + Request request, + ActionListener listener, + ActionFilterChain chain + ) { try { - if(threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN) == null) { + if (threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN) == null) { threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, Origin.LOCAL.toString()); } @@ -169,7 +190,7 @@ private void ap final Set injectedRoles = rolesInjector.injectUserAndRoles(request, action, task, threadContext); boolean enforcePrivilegesEvaluation = false; User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - if(user == null && (user = userInjector.getInjectedUser()) != null) { + if (user == null && (user = userInjector.getInjectedUser()) != null) { threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); // since there is no support for TransportClient auth/auth in 2.0 anymore, usually we // can skip any checks on transport in case of trusted requests. @@ -180,14 +201,14 @@ private void ap 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 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"); + 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()); @@ -196,35 +217,72 @@ private void ap if (isActionTraceEnabled()) { String count = ""; - if(request instanceof BulkRequest) { - count = ""+((BulkRequest) request).requests().size(); + if (request instanceof BulkRequest) { + count = "" + ((BulkRequest) request).requests().size(); } - if(request instanceof MultiGetRequest) { - count = ""+((MultiGetRequest) request).getItems().size(); + if (request instanceof MultiGetRequest) { + count = "" + ((MultiGetRequest) request).getItems().size(); } - if(request instanceof MultiSearchRequest) { - count = ""+((MultiSearchRequest) request).requests().size(); + if (request instanceof MultiSearchRequest) { + count = "" + ((MultiSearchRequest) request).requests().size(); } - traceAction("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()))); - + traceAction( + "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){ - - if(userIsAdmin && !confRequest && !internalRequest && !passThroughRequest) { + if (userIsAdmin && !confRequest && !internalRequest && !passThroughRequest) { auditLog.logGrantedPrivileges(action, request, task); auditLog.logIndexEvent(action, request, task); } @@ -233,15 +291,14 @@ private void ap return; } - - if(immutableIndicesMatcher != WildcardMatcher.NONE) { + if (immutableIndicesMatcher != WildcardMatcher.NONE) { boolean isImmutable = false; - if(request instanceof BulkShardRequest) { - for(BulkItemRequest bsr: ((BulkShardRequest) request).items()) { + if (request instanceof BulkShardRequest) { + for (BulkItemRequest bsr : ((BulkShardRequest) request).items()) { isImmutable = checkImmutableIndices(bsr.request(), listener); - if(isImmutable) { + if (isImmutable) { break; } } @@ -249,50 +306,67 @@ private void ap isImmutable = checkImmutableIndices(request, listener); } - if(isImmutable) { + if (isImmutable) { return; } } - if(Origin.LOCAL.toString().equals(threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)) - && (interClusterRequest || HeaderHelper.isDirectRequest(threadContext)) - && (injectedRoles == null) - && !enforcePrivilegesEvaluation - ) { + if (Origin.LOCAL.toString().equals(threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN)) + && (interClusterRequest || HeaderHelper.isDirectRequest(threadContext)) + && (injectedRoles == null) + && !enforcePrivilegesEvaluation) { chain.proceed(task, action, request, listener); return; } - if(user == null) { + if (user == null) { - if(action.startsWith("cluster:monitor/state")) { + if (action.startsWith("cluster:monitor/state")) { chain.proceed(task, action, request, listener); return; } - boolean skipSecurityIfDualMode = threadContext.getTransient(ConfigConstants.SECURITY_SSL_DUAL_MODE_SKIP_SECURITY) == Boolean.TRUE; - if((interClusterRequest || trustedClusterRequest || request.remoteAddress() == null) && !compatConfig.transportInterClusterAuthEnabled()) { + boolean skipSecurityIfDualMode = threadContext.getTransient( + ConfigConstants.SECURITY_SSL_DUAL_MODE_SKIP_SECURITY + ) == Boolean.TRUE; + if ((interClusterRequest || trustedClusterRequest || request.remoteAddress() == null) + && !compatConfig.transportInterClusterAuthEnabled()) { chain.proceed(task, action, request, listener); return; - } else if((interClusterRequest || trustedClusterRequest || request.remoteAddress() == null || skipSecurityIfDualMode) && compatConfig.transportInterClusterPassiveAuthEnabled()) { - log.info("Transport auth in passive mode and no user found. Injecting default user"); - user = User.DEFAULT_TRANSPORT_USER; - threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); - } else { - 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 OpenSearchSecurityException("No user found for "+action, RestStatus.INTERNAL_SERVER_ERROR)); - return; - } + } else if ((interClusterRequest || trustedClusterRequest || request.remoteAddress() == null || skipSecurityIfDualMode) + && compatConfig.transportInterClusterPassiveAuthEnabled()) { + log.info("Transport auth in passive mode and no user found. Injecting default user"); + user = User.DEFAULT_TRANSPORT_USER; + threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_USER, user); + } else { + 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 OpenSearchSecurityException("No user found for " + action, RestStatus.INTERNAL_SERVER_ERROR) + ); + return; + } } final PrivilegesEvaluator eval = evalp; if (!eval.isInitialized()) { log.error("OpenSearch Security not initialized for {}", action); - listener.onFailure(new OpenSearchSecurityException("OpenSearch Security not initialized for " - + action, RestStatus.SERVICE_UNAVAILABLE)); + listener.onFailure( + new OpenSearchSecurityException("OpenSearch Security not initialized for " + action, RestStatus.SERVICE_UNAVAILABLE) + ); return; } @@ -317,18 +391,30 @@ private void ap chain.proceed(task, action, request, listener); } else { CreateIndexRequest createIndexRequest = createIndexRequestBuilder.request(); - log.info("Request {} requires new tenant index {} with aliases {}", - request.getClass().getSimpleName(), createIndexRequest.index(), alias2Name(createIndexRequest.aliases())); + log.info( + "Request {} requires new tenant index {} with aliases {}", + request.getClass().getSimpleName(), + createIndexRequest.index(), + alias2Name(createIndexRequest.aliases()) + ); createIndexRequestBuilder.execute(new ActionListener() { @Override public void onResponse(CreateIndexResponse createIndexResponse) { if (createIndexResponse.isAcknowledged()) { - log.debug("Request to create index {} with aliases {} acknowledged, proceeding with {}", - createIndexRequest.index(), alias2Name(createIndexRequest.aliases()), request.getClass().getSimpleName()); + log.debug( + "Request to create index {} with aliases {} acknowledged, proceeding with {}", + createIndexRequest.index(), + alias2Name(createIndexRequest.aliases()), + request.getClass().getSimpleName() + ); chain.proceed(task, action, request, listener); } else { - String message = LoggerMessageFormat.format("Request to create index {} with aliases {} was not acknowledged, failing {}", - createIndexRequest.index(), alias2Name(createIndexRequest.aliases()), request.getClass().getSimpleName()); + String message = LoggerMessageFormat.format( + "Request to create index {} with aliases {} was not acknowledged, failing {}", + createIndexRequest.index(), + alias2Name(createIndexRequest.aliases()), + request.getClass().getSimpleName() + ); log.error(message); listener.onFailure(new OpenSearchException(message)); } @@ -338,12 +424,22 @@ public void onResponse(CreateIndexResponse createIndexResponse) { public void onFailure(Exception e) { Throwable cause = ExceptionsHelper.unwrapCause(e); if (cause instanceof ResourceAlreadyExistsException) { - log.warn("Request to create index {} with aliases {} failed as the resource already exists, proceeding with {}", - createIndexRequest.index(), alias2Name(createIndexRequest.aliases()), request.getClass().getSimpleName(), e); + log.warn( + "Request to create index {} with aliases {} failed as the resource already exists, proceeding with {}", + createIndexRequest.index(), + alias2Name(createIndexRequest.aliases()), + request.getClass().getSimpleName(), + e + ); chain.proceed(task, action, request, listener); } else { - log.error("Request to create index {} with aliases {} failed, failing {}", - createIndexRequest.index(), alias2Name(createIndexRequest.aliases()), request.getClass().getSimpleName(), e); + log.error( + "Request to create index {} with aliases {} failed, failing {}", + createIndexRequest.index(), + alias2Name(createIndexRequest.aliases()), + request.getClass().getSimpleName(), + e + ); listener.onFailure(e); } } @@ -352,12 +448,16 @@ public void onFailure(Exception e) { } else { auditLog.logMissingPrivileges(action, request, task); String err; - if(!pres.getMissingSecurityRoles().isEmpty()) { + if (!pres.getMissingSecurityRoles().isEmpty()) { err = String.format("No mapping for %s on roles %s", user, pres.getMissingSecurityRoles()); } else { - err = (injectedRoles != null) ? - String.format("no permissions for %s and associated roles %s", pres.getMissingPrivileges(), pres.getResolvedSecurityRoles()) : - String.format("no permissions for %s and %s", pres.getMissingPrivileges(), user); + err = (injectedRoles != null) + ? String.format( + "no permissions for %s and associated roles %s", + pres.getMissingPrivileges(), + pres.getResolvedSecurityRoles() + ) + : String.format("no permissions for %s and %s", pres.getMissingPrivileges(), user); } log.debug(err); listener.onFailure(new OpenSearchSecurityException(err, RestStatus.FORBIDDEN)); @@ -370,7 +470,7 @@ public void onFailure(Exception e) { } listener.onFailure(e); } catch (Throwable e) { - log.error("Unexpected exception "+e, e); + log.error("Unexpected exception " + e, e); listener.onFailure(new OpenSearchSecurityException("Unexpected exception " + action, RestStatus.INTERNAL_SERVER_ERROR)); } } @@ -385,13 +485,13 @@ private static boolean isUserAdmin(User user, final AdminDNs adminDns) { private void attachSourceFieldContext(ActionRequest request) { - if(request instanceof SearchRequest && SourceFieldsContext.isNeeded((SearchRequest) request)) { - if(threadContext.getHeader("_opendistro_security_source_field_context") == null) { + 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) { + 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); } @@ -401,13 +501,13 @@ private void attachSourceFieldContext(ActionRequest request) { @SuppressWarnings("rawtypes") private boolean checkImmutableIndices(Object request, ActionListener listener) { final boolean isModifyIndexRequest = request instanceof DeleteRequest - || request instanceof UpdateRequest - || request instanceof UpdateByQueryRequest - || request instanceof DeleteByQueryRequest - || request instanceof DeleteIndexRequest - || request instanceof RestoreSnapshotRequest - || request instanceof CloseIndexRequest - || request instanceof IndicesAliasesRequest; + || request instanceof UpdateRequest + || request instanceof UpdateByQueryRequest + || request instanceof DeleteByQueryRequest + || request instanceof DeleteIndexRequest + || request instanceof RestoreSnapshotRequest + || request instanceof CloseIndexRequest + || request instanceof IndicesAliasesRequest; if (isModifyIndexRequest && isRequestIndexImmutable(request)) { listener.onFailure(new OpenSearchSecurityException("Index is immutable", RestStatus.FORBIDDEN)); diff --git a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java index 25926dab32..80bba54ea2 100644 --- a/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java +++ b/src/main/java/org/opensearch/security/filter/SecurityRestFilter.java @@ -83,13 +83,18 @@ public class SecurityRestFilter { private static final String HEALTH_SUFFIX = "health"; private static final String WHO_AM_I_SUFFIX = "whoami"; - private static final String REGEX_PATH_PREFIX = "/("+ LEGACY_OPENDISTRO_PREFIX + "|" + PLUGINS_PREFIX + ")/" +"(.*)"; + private static final String REGEX_PATH_PREFIX = "/(" + LEGACY_OPENDISTRO_PREFIX + "|" + PLUGINS_PREFIX + ")/" + "(.*)"; private static final Pattern PATTERN_PATH_PREFIX = Pattern.compile(REGEX_PATH_PREFIX); - - public SecurityRestFilter(final BackendRegistry registry, final AuditLog auditLog, - final ThreadPool threadPool, final PrincipalExtractor principalExtractor, - final Settings settings, final Path configPath, final CompatConfig compatConfig) { + public SecurityRestFilter( + 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; @@ -123,8 +128,9 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c org.apache.logging.log4j.ThreadContext.clearAll(); if (!checkAndAuthenticateRequest(request, channel, client)) { User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - if (userIsSuperAdmin(user, adminDNs) || (whitelistingSettings.checkRequestIsAllowed(request, channel, client) && allowlistingSettings.checkRequestIsAllowed(request, channel, client))) { - //TODO: If the request is going to the ext, issue a JWT for authenticated user. + if (userIsSuperAdmin(user, adminDNs) + || (whitelistingSettings.checkRequestIsAllowed(request, channel, client) + && allowlistingSettings.checkRequestIsAllowed(request, channel, client))) { original.handleRequest(request, channel, client); } } @@ -139,12 +145,11 @@ private boolean userIsSuperAdmin(User user, AdminDNs adminDNs) { return user != null && adminDNs.isAdmin(user); } - private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel channel, - NodeClient client) throws Exception { + private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel channel, NodeClient client) throws Exception { threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_ORIGIN, Origin.REST.toString()); - if(HTTPHelper.containsBadHeader(request)) { + if (HTTPHelper.containsBadHeader(request)) { final OpenSearchException exception = ExceptionUtils.createBadHeaderException(); log.error(exception.toString()); auditLog.logBadHeaders(request); @@ -152,7 +157,7 @@ private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel cha return true; } - if(SSLRequestHelper.containsBadHeader(threadContext, ConfigConstants.OPENDISTRO_SECURITY_CONFIG_PREFIX)) { + if (SSLRequestHelper.containsBadHeader(threadContext, ConfigConstants.OPENDISTRO_SECURITY_CONFIG_PREFIX)) { final OpenSearchException exception = ExceptionUtils.createBadHeaderException(); log.error(exception.toString()); auditLog.logBadHeaders(request); @@ -162,13 +167,13 @@ private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel cha final SSLInfo sslInfo; try { - if((sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, request, principalExtractor)) != null) { - if(sslInfo.getPrincipal() != null) { + 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()); + 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()); @@ -180,22 +185,23 @@ private boolean checkAndAuthenticateRequest(RestRequest request, RestChannel cha return true; } - if(!compatConfig.restAuthEnabled()) { + if (!compatConfig.restAuthEnabled()) { return false; } Matcher matcher = PATTERN_PATH_PREFIX.matcher(request.path()); final String suffix = matcher.matches() ? matcher.group(2) : null; - if(request.method() != Method.OPTIONS - && !(HEALTH_SUFFIX.equals(suffix)) - && !(WHO_AM_I_SUFFIX.equals(suffix))) { + if (request.method() != Method.OPTIONS && !(HEALTH_SUFFIX.equals(suffix)) && !(WHO_AM_I_SUFFIX.equals(suffix))) { 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()); + org.apache.logging.log4j.ThreadContext.put( + "user", + ((User) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER)).getName() + ); } } diff --git a/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java index 30e6134381..88ac128828 100644 --- a/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPBasicAuthenticator.java @@ -55,7 +55,7 @@ public AuthCredentials extractCredentials(final RestRequest request, ThreadConte final boolean forceLogin = request.paramAsBoolean("force_login", false); - if(forceLogin) { + if (forceLogin) { return null; } diff --git a/src/main/java/org/opensearch/security/http/HTTPClientCertAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPClientCertAuthenticator.java index 373919669d..b1e5d4ef40 100644 --- a/src/main/java/org/opensearch/security/http/HTTPClientCertAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPClientCertAuthenticator.java @@ -71,16 +71,16 @@ public AuthCredentials extractCredentials(final RestRequest request, final Threa String username = principal.trim(); String[] backendRoles = null; - if(usernameAttribute != null && usernameAttribute.length() > 0) { + if (usernameAttribute != null && usernameAttribute.length() > 0) { final List usernames = getDnAttribute(rfc2253dn, usernameAttribute); - if(usernames.isEmpty() == false) { + if (usernames.isEmpty() == false) { username = usernames.get(0); } } - if(rolesAttribute != null && rolesAttribute.length() > 0) { + if (rolesAttribute != null && rolesAttribute.length() > 0) { final List roles = getDnAttribute(rfc2253dn, rolesAttribute); - if(roles.isEmpty() == false) { + if (roles.isEmpty() == false) { backendRoles = roles.toArray(new String[0]); } } diff --git a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java index 1fabd0874c..c980956fb8 100644 --- a/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPOnBehalfOfJwtAuthenticator.java @@ -51,7 +51,7 @@ public class HTTPOnBehalfOfJwtAuthenticator implements HTTPAuthenticator { private static final Pattern BEARER = Pattern.compile("^\\s*Bearer\\s.*", Pattern.CASE_INSENSITIVE); private static final String BEARER_PREFIX = "bearer "; - //TODO: TO SEE IF WE NEED THE FINAL FOR FOLLOWING + // TODO: TO SEE IF WE NEED THE FINAL FOR FOLLOWING private JwtParser jwtParser; private String subjectKey; @@ -64,7 +64,7 @@ public HTTPOnBehalfOfJwtAuthenticator() { } // FOR TESTING - public HTTPOnBehalfOfJwtAuthenticator(String signingKey, String encryptionKey){ + public HTTPOnBehalfOfJwtAuthenticator(String signingKey, String encryptionKey) { this.signingKey = signingKey; this.encryptionKey = encryptionKey; init(); @@ -73,7 +73,7 @@ public HTTPOnBehalfOfJwtAuthenticator(String signingKey, String encryptionKey){ private void init() { try { - if(signingKey == null || signingKey.length() == 0) { + if (signingKey == null || signingKey.length() == 0) { log.error("signingKey must not be null or empty. JWT authentication will not work"); } else { @@ -95,7 +95,7 @@ private void init() { log.debug("No public ECDSA key, try other algos ({})", e.toString()); } - if(key != null) { + if (key != null) { jwtParser = Jwts.parser().setSigningKey(key); } else { jwtParser = Jwts.parser().setSigningKey(decoded); @@ -138,7 +138,7 @@ private AuthCredentials extractCredentials0(final RestRequest request) { String jwtToken = request.header(HttpHeaders.AUTHORIZATION); if (jwtToken == null || jwtToken.length() == 0) { - if(log.isDebugEnabled()) { + if (log.isDebugEnabled()) { log.debug("No JWT token found in '{}' header", HttpHeaders.AUTHORIZATION); } return null; @@ -149,10 +149,10 @@ private AuthCredentials extractCredentials0(final RestRequest request) { } final int index; - if((index = jwtToken.toLowerCase().indexOf(BEARER_PREFIX)) > -1) { //detect Bearer - jwtToken = jwtToken.substring(index+BEARER_PREFIX.length()); + if ((index = jwtToken.toLowerCase().indexOf(BEARER_PREFIX)) > -1) { // detect Bearer + jwtToken = jwtToken.substring(index + BEARER_PREFIX.length()); } else { - if(log.isDebugEnabled()) { + if (log.isDebugEnabled()) { log.debug("No Bearer scheme found in header"); } } @@ -164,14 +164,14 @@ private AuthCredentials extractCredentials0(final RestRequest request) { final String audience = claims.getAudience(); - //TODO: GET ROLESCLAIM DEPENDING ON THE STATUS OF BWC MODE. ON: er / OFF: dr + // TODO: GET ROLESCLAIM DEPENDING ON THE STATUS OF BWC MODE. ON: er / OFF: dr Object rolesObject = null; String[] roles; try { rolesObject = claims.get("er"); } catch (Throwable e) { - log.debug("No encrypted role founded in the claim, continue searching for decrypted roles."); + log.debug("No encrypted role founded in the claim, continue searching for decrypted roles."); } try { @@ -181,8 +181,7 @@ private AuthCredentials extractCredentials0(final RestRequest request) { } if (rolesObject == null) { - log.warn( - "Failed to get roles from JWT claims. Check if this key is correct and available in the JWT payload."); + log.warn("Failed to get roles from JWT claims. Check if this key is correct and available in the JWT payload."); roles = new String[0]; } else { final String rolesClaim = rolesObject.toString(); @@ -190,7 +189,7 @@ private AuthCredentials extractCredentials0(final RestRequest request) { // Extracting roles based on the compatbility mode String decryptedRoles = rolesClaim; if (rolesObject == claims.get("er")) { - //TODO: WHERE TO GET THE ENCRYTION KEY + // TODO: WHERE TO GET THE ENCRYTION KEY decryptedRoles = EncryptionDecryptionUtil.decrypt(encryptionKey, rolesClaim); } roles = Arrays.stream(decryptedRoles.split(",")).map(String::trim).toArray(String[]::new); @@ -207,8 +206,8 @@ private AuthCredentials extractCredentials0(final RestRequest request) { final AuthCredentials ac = new AuthCredentials(subject, roles).markComplete(); - for(Entry claim: claims.entrySet()) { - ac.addAttribute("attr.jwt."+claim.getKey(), String.valueOf(claim.getValue())); + for (Entry claim : claims.entrySet()) { + ac.addAttribute("attr.jwt." + claim.getKey(), String.valueOf(claim.getValue())); } return ac; @@ -217,7 +216,7 @@ private AuthCredentials extractCredentials0(final RestRequest request) { log.error("Cannot authenticate user with JWT because of ", e); return null; } catch (Exception e) { - if(log.isDebugEnabled()) { + if (log.isDebugEnabled()) { log.debug("Invalid or expired JWT token.", e); } return null; @@ -234,27 +233,33 @@ public String getType() { return "onbehalfof_jwt"; } - //TODO: Extract the audience (ext_id) and inject it into thread context + // TODO: Extract the audience (ext_id) and inject it into thread context protected String extractSubject(final Claims claims, final RestRequest request) { String subject = claims.getSubject(); - if(subjectKey != null) { + if (subjectKey != null) { // try to get roles from claims, first as Object to avoid having to catch the ExpectedTypeException Object subjectObject = claims.get(subjectKey, Object.class); - if(subjectObject == null) { + if (subjectObject == null) { log.warn("Failed to get subject from JWT claims, check if subject_key '{}' is correct.", subjectKey); return null; } // We expect a String. If we find something else, convert to String but issue a warning - if(!(subjectObject instanceof String)) { - log.warn("Expected type String in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.", subjectKey, subjectObject, subjectObject.getClass()); + if (!(subjectObject instanceof String)) { + log.warn( + "Expected type String in the JWT for subject_key {}, but value was '{}' ({}). Will convert this value to String.", + subjectKey, + subjectObject, + subjectObject.getClass() + ); } subject = String.valueOf(subjectObject); } return subject; } - private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, InvalidKeySpecException { + private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) throws NoSuchAlgorithmException, + InvalidKeySpecException { X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); KeyFactory kf = KeyFactory.getInstance(algo); return kf.generatePublic(spec); @@ -263,8 +268,8 @@ private static PublicKey getPublicKey(final byte[] keyBytes, final String algo) @Subscribe public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { - //TODO: #2615 FOR CONFIGURATION - //For Testing + // TODO: #2615 FOR CONFIGURATION + // For Testing signingKey = "abcd1234"; encryptionKey = RandomStringUtils.randomAlphanumeric(16); } diff --git a/src/main/java/org/opensearch/security/http/HTTPProxyAuthenticator.java b/src/main/java/org/opensearch/security/http/HTTPProxyAuthenticator.java index 348811b694..a58a842394 100644 --- a/src/main/java/org/opensearch/security/http/HTTPProxyAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/HTTPProxyAuthenticator.java @@ -52,13 +52,13 @@ public class HTTPProxyAuthenticator implements HTTPAuthenticator { public HTTPProxyAuthenticator(Settings settings, final Path configPath) { super(); this.settings = settings; - this.rolesSeparator = Pattern.compile(settings.get("roles_separator", ",")); + this.rolesSeparator = Pattern.compile(settings.get("roles_separator", ",")); } @Override public AuthCredentials extractCredentials(final RestRequest request, ThreadContext context) { - if(context.getTransient(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE) != Boolean.TRUE) { + if (context.getTransient(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE) != Boolean.TRUE) { throw new OpenSearchSecurityException("xff not done"); } @@ -76,11 +76,10 @@ public AuthCredentials extractCredentials(final RestRequest request, ThreadConte String[] backendRoles = null; if (!Strings.isNullOrEmpty(rolesHeader) && !Strings.isNullOrEmpty((String) request.header(rolesHeader))) { - backendRoles = rolesSeparator - .splitAsStream((String) request.header(rolesHeader)) - .map(String::trim) - .filter(Predicates.not(String::isEmpty)) - .toArray(String[]::new); + backendRoles = rolesSeparator.splitAsStream((String) request.header(rolesHeader)) + .map(String::trim) + .filter(Predicates.not(String::isEmpty)) + .toArray(String[]::new); } return new AuthCredentials((String) request.header(userHeader), backendRoles).markComplete(); } else { diff --git a/src/main/java/org/opensearch/security/http/RemoteIpDetector.java b/src/main/java/org/opensearch/security/http/RemoteIpDetector.java index 5d9e933c8f..f464c0653a 100644 --- a/src/main/java/org/opensearch/security/http/RemoteIpDetector.java +++ b/src/main/java/org/opensearch/security/http/RemoteIpDetector.java @@ -73,21 +73,23 @@ final class RemoteIpDetector { * @return array of String (non null) */ protected static String[] commaDelimitedListToStringArray(String commaDelimitedStrings) { - return (commaDelimitedStrings == null || commaDelimitedStrings.length() == 0) ? new String[0] : commaSeparatedValuesPattern - .split(commaDelimitedStrings); + return (commaDelimitedStrings == null || commaDelimitedStrings.length() == 0) + ? new String[0] + : commaSeparatedValuesPattern.split(commaDelimitedStrings); } /** * @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}"); + "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 #setRemoteIpHeader(String) @@ -113,31 +115,30 @@ public String getRemoteIpHeader() { return remoteIpHeader; } - String detect(RestRequest request, ThreadContext threadContext){ - final String originalRemoteAddr = ((InetSocketAddress)request.getHttpChannel().getRemoteAddress()).getAddress().getHostAddress(); + String detect(RestRequest request, ThreadContext threadContext) { + final String originalRemoteAddr = ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress().getHostAddress(); final boolean isTraceEnabled = log.isTraceEnabled(); if (isTraceEnabled) { log.trace("originalRemoteAddr {}", originalRemoteAddr); } - //X-Forwarded-For: client1, proxy1, proxy2 - // ^^^^^^ originalRemoteAddr + // X-Forwarded-For: client1, proxy1, proxy2 + // ^^^^^^ originalRemoteAddr - //originalRemoteAddr need to be in the list of internalProxies - if (internalProxies !=null && - internalProxies.matcher(originalRemoteAddr).matches()) { + // originalRemoteAddr need to be in the list of internalProxies + if (internalProxies != null && internalProxies.matcher(originalRemoteAddr).matches()) { String remoteIp = null; final StringBuilder concatRemoteIpHeaderValue = new StringBuilder(); - //client1, proxy1, proxy2 - final List remoteIpHeaders = request.getHeaders().get(remoteIpHeader); //X-Forwarded-For + // client1, proxy1, proxy2 + final List remoteIpHeaders = request.getHeaders().get(remoteIpHeader); // X-Forwarded-For - if(remoteIpHeaders == null || remoteIpHeaders.isEmpty()) { + if (remoteIpHeaders == null || remoteIpHeaders.isEmpty()) { return originalRemoteAddr; } - for (String rh:remoteIpHeaders) { + for (String rh : remoteIpHeaders) { if (concatRemoteIpHeaderValue.length() > 0) { concatRemoteIpHeaderValue.append(", "); } @@ -172,8 +173,15 @@ String detect(RestRequest request, ThreadContext threadContext){ if (remoteIp != null) { if (isTraceEnabled) { - final String originalRemoteHost = ((InetSocketAddress)request.getHttpChannel().getRemoteAddress()).getAddress().getHostName(); - log.trace("Incoming request {} with originalRemoteAddr '{}', originalRemoteHost='{}', will be seen as newRemoteAddr='{}'", request.uri(), originalRemoteAddr, originalRemoteHost, remoteIp); + final String originalRemoteHost = ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getAddress() + .getHostName(); + log.trace( + "Incoming request {} with originalRemoteAddr '{}', originalRemoteHost='{}', will be seen as newRemoteAddr='{}'", + request.uri(), + originalRemoteAddr, + originalRemoteHost, + remoteIp + ); } threadContext.putTransient(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE, Boolean.TRUE); @@ -185,7 +193,11 @@ String detect(RestRequest request, ThreadContext threadContext){ } else { if (isTraceEnabled) { - log.trace("Skip RemoteIpDetector for request {} with originalRemoteAddr '{}' cause no internal proxy matches", request.uri(), request.getHttpChannel().getRemoteAddress()); + log.trace( + "Skip RemoteIpDetector for request {} with originalRemoteAddr '{}' cause no internal proxy matches", + request.uri(), + request.getHttpChannel().getRemoteAddress() + ); } } diff --git a/src/main/java/org/opensearch/security/http/SecurityHttpServerTransport.java b/src/main/java/org/opensearch/security/http/SecurityHttpServerTransport.java index 6f2f57053f..e9487a49a9 100644 --- a/src/main/java/org/opensearch/security/http/SecurityHttpServerTransport.java +++ b/src/main/java/org/opensearch/security/http/SecurityHttpServerTransport.java @@ -40,9 +40,29 @@ public class SecurityHttpServerTransport extends SecuritySSLNettyHttpServerTransport { - public SecurityHttpServerTransport(final Settings settings, final NetworkService networkService, - final BigArrays bigArrays, final ThreadPool threadPool, final SecurityKeyStore odsks, - final SslExceptionHandler sslExceptionHandler, final NamedXContentRegistry namedXContentRegistry, final ValidatingDispatcher dispatcher, final ClusterSettings clusterSettings, SharedGroupFactory sharedGroupFactory) { - super(settings, networkService, bigArrays, threadPool, odsks, namedXContentRegistry, dispatcher, sslExceptionHandler, clusterSettings, sharedGroupFactory); + public SecurityHttpServerTransport( + final Settings settings, + final NetworkService networkService, + final BigArrays bigArrays, + final ThreadPool threadPool, + final SecurityKeyStore odsks, + final SslExceptionHandler sslExceptionHandler, + final NamedXContentRegistry namedXContentRegistry, + final ValidatingDispatcher dispatcher, + final ClusterSettings clusterSettings, + SharedGroupFactory sharedGroupFactory + ) { + super( + settings, + networkService, + bigArrays, + threadPool, + odsks, + namedXContentRegistry, + dispatcher, + sslExceptionHandler, + clusterSettings, + sharedGroupFactory + ); } } diff --git a/src/main/java/org/opensearch/security/http/SecurityNonSslHttpServerTransport.java b/src/main/java/org/opensearch/security/http/SecurityNonSslHttpServerTransport.java index 3c1dedc55e..1c21f0c4a2 100644 --- a/src/main/java/org/opensearch/security/http/SecurityNonSslHttpServerTransport.java +++ b/src/main/java/org/opensearch/security/http/SecurityNonSslHttpServerTransport.java @@ -41,10 +41,16 @@ public class SecurityNonSslHttpServerTransport extends Netty4HttpServerTransport { - - public SecurityNonSslHttpServerTransport(final Settings settings, final NetworkService networkService, final BigArrays bigArrays, - final ThreadPool threadPool, final NamedXContentRegistry namedXContentRegistry, final Dispatcher dispatcher, - ClusterSettings clusterSettings, SharedGroupFactory sharedGroupFactory) { + public SecurityNonSslHttpServerTransport( + final Settings settings, + final NetworkService networkService, + final BigArrays bigArrays, + final ThreadPool threadPool, + final NamedXContentRegistry namedXContentRegistry, + final Dispatcher dispatcher, + ClusterSettings clusterSettings, + SharedGroupFactory sharedGroupFactory + ) { super(settings, networkService, bigArrays, threadPool, namedXContentRegistry, dispatcher, clusterSettings, sharedGroupFactory); } diff --git a/src/main/java/org/opensearch/security/http/XFFResolver.java b/src/main/java/org/opensearch/security/http/XFFResolver.java index c44e98537d..aff5043f61 100644 --- a/src/main/java/org/opensearch/security/http/XFFResolver.java +++ b/src/main/java/org/opensearch/security/http/XFFResolver.java @@ -59,39 +59,48 @@ public TransportAddress resolve(final RestRequest request) throws OpenSearchSecu log.trace("resolve {}", request.getHttpChannel().getRemoteAddress()); } - if(enabled && request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress && request.getHttpChannel() instanceof Netty4HttpChannel) { + if (enabled + && request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress + && request.getHttpChannel() instanceof Netty4HttpChannel) { - final InetSocketAddress isa = new InetSocketAddress(detector.detect(request, threadContext), ((InetSocketAddress)request.getHttpChannel().getRemoteAddress()).getPort()); + final InetSocketAddress isa = new InetSocketAddress( + detector.detect(request, threadContext), + ((InetSocketAddress) request.getHttpChannel().getRemoteAddress()).getPort() + ); - if(isa.isUnresolved()) { - throw new OpenSearchSecurityException("Cannot resolve address "+isa.getHostString()); + if (isa.isUnresolved()) { + throw new OpenSearchSecurityException("Cannot resolve address " + isa.getHostString()); } - if (isTraceEnabled) { - if(threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE) == Boolean.TRUE) { + if (threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_XFF_DONE) == Boolean.TRUE) { log.trace("xff resolved {} to {}", request.getHttpChannel().getRemoteAddress(), isa); } else { - log.trace("no xff done for {}",request.getClass()); + log.trace("no xff done for {}", request.getClass()); } } return new TransportAddress(isa); - } else if(request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress){ + } else if (request.getHttpChannel().getRemoteAddress() instanceof InetSocketAddress) { if (isTraceEnabled) { - log.trace("no xff done (enabled or no netty request) {},{},{},{}",enabled, request.getClass()); + log.trace("no xff done (enabled or no netty request) {},{},{},{}", enabled, request.getClass()); } - return new TransportAddress((InetSocketAddress)request.getHttpChannel().getRemoteAddress()); + return new TransportAddress((InetSocketAddress) request.getHttpChannel().getRemoteAddress()); } else { - throw new OpenSearchSecurityException("Cannot handle this request. Remote address is "+request.getHttpChannel().getRemoteAddress()+" with request class "+request.getClass()); + throw new OpenSearchSecurityException( + "Cannot handle this request. Remote address is " + + request.getHttpChannel().getRemoteAddress() + + " with request class " + + request.getClass() + ); } } @Subscribe public void onDynamicConfigModelChanged(DynamicConfigModel dcm) { enabled = dcm.isXffEnabled(); - if(enabled) { + if (enabled) { detector = new RemoteIpDetector(); detector.setInternalProxies(dcm.getInternalProxies()); detector.setRemoteIpHeader(dcm.getRemoteIpHeader()); diff --git a/src/main/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticator.java b/src/main/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticator.java index e98f26d85a..ef20374d69 100644 --- a/src/main/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticator.java +++ b/src/main/java/org/opensearch/security/http/proxy/HTTPExtendedProxyAuthenticator.java @@ -42,7 +42,7 @@ import org.opensearch.security.http.HTTPProxyAuthenticator; import org.opensearch.security.user.AuthCredentials; -public class HTTPExtendedProxyAuthenticator extends HTTPProxyAuthenticator{ +public class HTTPExtendedProxyAuthenticator extends HTTPProxyAuthenticator { private static final String ATTR_PROXY = "attr.proxy."; private static final String ATTR_PROXY_USERNAME = "attr.proxy.username"; @@ -56,16 +56,16 @@ public HTTPExtendedProxyAuthenticator(Settings settings, final Path configPath) @Override public AuthCredentials extractCredentials(final RestRequest request, ThreadContext context) { - AuthCredentials credentials = super.extractCredentials(request, context); - if(credentials == null) { - return null; - } + AuthCredentials credentials = super.extractCredentials(request, context); + if (credentials == null) { + return null; + } String attrHeaderPrefix = settings.get("attr_header_prefix"); - if(Strings.isNullOrEmpty(attrHeaderPrefix)) { + if (Strings.isNullOrEmpty(attrHeaderPrefix)) { log.debug("attr_header_prefix is null. Skipping additional attribute extraction"); return credentials; - } else if(log.isDebugEnabled()) { + } else if (log.isDebugEnabled()) { log.debug("attrHeaderPrefix {}", attrHeaderPrefix); } @@ -73,10 +73,10 @@ public AuthCredentials extractCredentials(final RestRequest request, ThreadConte attrHeaderPrefix = attrHeaderPrefix.toLowerCase(); for (Entry> entry : request.getHeaders().entrySet()) { String key = entry.getKey().toLowerCase(); - if(key.startsWith(attrHeaderPrefix)) { + if (key.startsWith(attrHeaderPrefix)) { key = ATTR_PROXY + key.substring(attrHeaderPrefix.length()); credentials.addAttribute(key, Joiner.on(",").join(entry.getValue().iterator())); - if(log.isTraceEnabled()) { + if (log.isTraceEnabled()) { log.trace("Found user custom attribute '{}'", key); } } diff --git a/src/main/java/org/opensearch/security/httpclient/HttpClient.java b/src/main/java/org/opensearch/security/httpclient/HttpClient.java index ad507ea47c..ba788a2c13 100644 --- a/src/main/java/org/opensearch/security/httpclient/HttpClient.java +++ b/src/main/java/org/opensearch/security/httpclient/HttpClient.java @@ -117,8 +117,18 @@ public HttpClientBuilder setSupportedCipherSuites(String[] cipherSuites) { } public HttpClient build() throws Exception { - return new HttpClient(trustStore, basicCredentials, keystore, keyPassword, keystoreAlias, verifyHostnames, ssl, - supportedProtocols, supportedCipherSuites, servers); + return new HttpClient( + trustStore, + basicCredentials, + keystore, + keyPassword, + keystoreAlias, + verifyHostnames, + ssl, + supportedProtocols, + supportedCipherSuites, + servers + ); } private static String encodeBasicHeader(final String username, final String password) { @@ -143,10 +153,19 @@ public static HttpClientBuilder builder(final String... servers) { private String[] supportedProtocols; private String[] supportedCipherSuites; - private HttpClient(final KeyStore trustStore, final String basicCredentials, final KeyStore keystore, - final char[] keyPassword, final String keystoreAlias, final boolean verifyHostnames, final boolean ssl, String[] supportedProtocols, String[] supportedCipherSuites, final String... servers) - throws UnrecoverableKeyException, KeyManagementException, NoSuchAlgorithmException, KeyStoreException, CertificateException, - IOException { + private HttpClient( + final KeyStore trustStore, + final String basicCredentials, + final KeyStore keystore, + final char[] keyPassword, + final String keystoreAlias, + final boolean verifyHostnames, + final boolean ssl, + String[] supportedProtocols, + String[] supportedCipherSuites, + final String... servers + ) throws UnrecoverableKeyException, KeyManagementException, NoSuchAlgorithmException, KeyStoreException, CertificateException, + IOException { super(); this.trustStore = trustStore; this.basicCredentials = basicCredentials; @@ -159,13 +178,13 @@ private HttpClient(final KeyStore trustStore, final String basicCredentials, fin this.keystoreAlias = keystoreAlias; HttpHost[] hosts = Arrays.stream(servers) - .map(s->s.split(":")) - .map(s->new HttpHost(ssl?"https":"http", s[0], Integer.parseInt(s[1]))) - .collect(Collectors.toList()).toArray(new HttpHost[0]); - + .map(s -> s.split(":")) + .map(s -> new HttpHost(ssl ? "https" : "http", s[0], Integer.parseInt(s[1]))) + .collect(Collectors.toList()) + .toArray(new HttpHost[0]); RestClientBuilder builder = RestClient.builder(hosts); - //builder.setMaxRetryTimeoutMillis(10000); + // builder.setMaxRetryTimeoutMillis(10000); builder.setFailureListener(new RestClient.FailureListener() { @Override @@ -181,7 +200,7 @@ public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpCli try { return asyncClientBuilder(httpClientBuilder); } catch (Exception e) { - log.error("Unable to build http client",e); + log.error("Unable to build http client", e); throw new RuntimeException(e); } } @@ -192,24 +211,25 @@ public HttpAsyncClientBuilder customizeHttpClient(HttpAsyncClientBuilder httpCli public boolean index(final String content, final String index, final String type, final boolean refresh) { - try { + try { - final IndexRequest ir = new IndexRequest(index); + final IndexRequest ir = new IndexRequest(index); - final IndexResponse response = rclient.index(ir - .setRefreshPolicy(refresh?RefreshPolicy.IMMEDIATE:RefreshPolicy.NONE) - .source(content, XContentType.JSON), RequestOptions.DEFAULT); + final IndexResponse response = rclient.index( + ir.setRefreshPolicy(refresh ? RefreshPolicy.IMMEDIATE : RefreshPolicy.NONE).source(content, XContentType.JSON), + RequestOptions.DEFAULT + ); - return response.getShardInfo().getSuccessful() > 0 && response.getShardInfo().getFailed() == 0; + return response.getShardInfo().getSuccessful() > 0 && response.getShardInfo().getFailed() == 0; - } catch (Exception e) { - log.error(e.toString(),e); - return false; - } + } catch (Exception e) { + log.error(e.toString(), e); + return false; + } } - private final HttpAsyncClientBuilder asyncClientBuilder(HttpAsyncClientBuilder httpClientBuilder) - throws NoSuchAlgorithmException, KeyStoreException, UnrecoverableKeyException, KeyManagementException { + private final HttpAsyncClientBuilder asyncClientBuilder(HttpAsyncClientBuilder httpClientBuilder) throws NoSuchAlgorithmException, + KeyStoreException, UnrecoverableKeyException, KeyManagementException { // basic auth // pki auth @@ -231,11 +251,11 @@ private final HttpAsyncClientBuilder asyncClientBuilder(HttpAsyncClientBuilder h @Override public String chooseAlias(Map aliases, SSLParameters sslParameters) { - if(aliases == null || aliases.isEmpty()) { + if (aliases == null || aliases.isEmpty()) { return keystoreAlias; } - if(keystoreAlias == null || keystoreAlias.isEmpty()) { + if (keystoreAlias == null || keystoreAlias.isEmpty()) { return aliases.keySet().iterator().next(); } @@ -248,35 +268,36 @@ public String chooseAlias(Map aliases, SSLParameters final SSLContext sslContext = sslContextBuilder.build(); TlsStrategy tlsStrategy = ClientTlsStrategyBuilder.create() - .setSslContext(sslContext) - .setTlsVersions(supportedProtocols) - .setCiphers(supportedCipherSuites) - .setHostnameVerifier(hnv) - // See please https://issues.apache.org/jira/browse/HTTPCLIENT-2219 - .setTlsDetailsFactory(new Factory() { - @Override - public TlsDetails create(final SSLEngine sslEngine) { - return new TlsDetails(sslEngine.getSession(), sslEngine.getApplicationProtocol()); - } - }) - .build(); + .setSslContext(sslContext) + .setTlsVersions(supportedProtocols) + .setCiphers(supportedCipherSuites) + .setHostnameVerifier(hnv) + // See please https://issues.apache.org/jira/browse/HTTPCLIENT-2219 + .setTlsDetailsFactory(new Factory() { + @Override + public TlsDetails create(final SSLEngine sslEngine) { + return new TlsDetails(sslEngine.getSession(), sslEngine.getApplicationProtocol()); + } + }) + .build(); - final AsyncClientConnectionManager cm = PoolingAsyncClientConnectionManagerBuilder.create() - .setTlsStrategy(tlsStrategy) - .build(); + final AsyncClientConnectionManager cm = PoolingAsyncClientConnectionManagerBuilder.create().setTlsStrategy(tlsStrategy).build(); httpClientBuilder.setConnectionManager(cm); } if (basicCredentials != null) { - httpClientBuilder.setDefaultHeaders(Lists.newArrayList(new BasicHeader(HttpHeaders.AUTHORIZATION, "Basic " + basicCredentials))); + httpClientBuilder.setDefaultHeaders( + Lists.newArrayList(new BasicHeader(HttpHeaders.AUTHORIZATION, "Basic " + basicCredentials)) + ); } // TODO: set a timeout until we have a proper way to deal with back pressure int timeout = 5; RequestConfig config = RequestConfig.custom() - .setConnectTimeout(timeout, TimeUnit.SECONDS) - .setConnectionRequestTimeout(timeout, TimeUnit.SECONDS).build(); + .setConnectTimeout(timeout, TimeUnit.SECONDS) + .setConnectionRequestTimeout(timeout, TimeUnit.SECONDS) + .build(); httpClientBuilder.setDefaultRequestConfig(config); diff --git a/src/main/java/org/opensearch/security/privileges/PitPrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PitPrivilegesEvaluator.java index b146365d57..57c1c18414 100644 --- a/src/main/java/org/opensearch/security/privileges/PitPrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PitPrivilegesEvaluator.java @@ -30,7 +30,6 @@ import org.opensearch.security.securityconf.SecurityRoles; import org.opensearch.security.user.User; - /** * This class evaluates privileges for point in time (Delete and List all) operations. * For aliases - users must have either alias permission or backing index permissions @@ -38,13 +37,18 @@ */ public class PitPrivilegesEvaluator { - public PrivilegesEvaluatorResponse evaluate(final ActionRequest request, final ClusterService clusterService, - final User user, final SecurityRoles securityRoles, final String action, - final IndexNameExpressionResolver resolver, - final PrivilegesEvaluatorResponse presponse, - final IndexResolverReplacer irr) { + public PrivilegesEvaluatorResponse evaluate( + final ActionRequest request, + final ClusterService clusterService, + final User user, + final SecurityRoles securityRoles, + final String action, + final IndexNameExpressionResolver resolver, + final PrivilegesEvaluatorResponse presponse, + final IndexResolverReplacer irr + ) { - if(!(request instanceof DeletePitRequest || request instanceof PitSegmentsRequest)) { + if (!(request instanceof DeletePitRequest || request instanceof PitSegmentsRequest)) { return presponse; } List pitIds = new ArrayList<>(); @@ -52,7 +56,7 @@ public PrivilegesEvaluatorResponse evaluate(final ActionRequest request, final C if (request instanceof DeletePitRequest) { DeletePitRequest deletePitRequest = (DeletePitRequest) request; pitIds = deletePitRequest.getPitIds(); - } else if(request instanceof PitSegmentsRequest) { + } else if (request instanceof PitSegmentsRequest) { PitSegmentsRequest pitSegmentsRequest = (PitSegmentsRequest) request; pitIds = pitSegmentsRequest.getPitIds(); } @@ -60,29 +64,32 @@ public PrivilegesEvaluatorResponse evaluate(final ActionRequest request, final C if (pitIds.size() == 1 && "_all".equals(pitIds.get(0))) { return presponse; } else { - return handlePitsAccess(pitIds, clusterService, user, securityRoles, - action, resolver, presponse, irr); + return handlePitsAccess(pitIds, clusterService, user, securityRoles, action, resolver, presponse, irr); } } /** * Handle access for delete operation / pit segments operation where PIT IDs are explicitly passed */ - private PrivilegesEvaluatorResponse handlePitsAccess(List pitIds, ClusterService clusterService, - User user, SecurityRoles securityRoles, final String action, - IndexNameExpressionResolver resolver, PrivilegesEvaluatorResponse presponse, - final IndexResolverReplacer irr) { - Map pitToIndicesMap = OpenSearchSecurityPlugin. - GuiceHolder.getPitService().getIndicesForPits(pitIds); + private PrivilegesEvaluatorResponse handlePitsAccess( + List pitIds, + ClusterService clusterService, + User user, + SecurityRoles securityRoles, + final String action, + IndexNameExpressionResolver resolver, + PrivilegesEvaluatorResponse presponse, + final IndexResolverReplacer irr + ) { + Map pitToIndicesMap = OpenSearchSecurityPlugin.GuiceHolder.getPitService().getIndicesForPits(pitIds); Set pitIndices = new HashSet<>(); // add indices across all PITs to a set and evaluate if user has access to all indices - for(String[] indices: pitToIndicesMap.values()) { + for (String[] indices : pitToIndicesMap.values()) { pitIndices.addAll(Arrays.asList(indices)); } - Set allPermittedIndices = getPermittedIndices(pitIndices, clusterService, user, - securityRoles, action, resolver, irr); + Set allPermittedIndices = getPermittedIndices(pitIndices, clusterService, user, securityRoles, action, resolver, irr); // Only if user has access to all PIT's indices, allow operation, otherwise continue evaluation in PrivilegesEvaluator. - if(allPermittedIndices.containsAll(pitIndices)) { + if (allPermittedIndices.containsAll(pitIndices)) { presponse.allowed = true; presponse.markComplete(); } @@ -92,14 +99,18 @@ private PrivilegesEvaluatorResponse handlePitsAccess(List pitIds, Cluste /** * This method returns list of permitted indices for the PIT indices passed */ - private Set getPermittedIndices(Set pitIndices, ClusterService clusterService, - User user, SecurityRoles securityRoles, final String action, - IndexNameExpressionResolver resolver, final IndexResolverReplacer irr) { + private Set getPermittedIndices( + Set pitIndices, + ClusterService clusterService, + User user, + SecurityRoles securityRoles, + final String action, + IndexNameExpressionResolver resolver, + final IndexResolverReplacer irr + ) { String[] indicesArr = new String[pitIndices.size()]; - CreatePitRequest req = new CreatePitRequest(new TimeValue(1, TimeUnit.DAYS), true, - pitIndices.toArray(indicesArr)); + CreatePitRequest req = new CreatePitRequest(new TimeValue(1, TimeUnit.DAYS), true, pitIndices.toArray(indicesArr)); final IndexResolverReplacer.Resolved pitResolved = irr.resolveRequest(req); - return securityRoles.reduce(pitResolved, - user, new String[]{action}, resolver, clusterService); + return securityRoles.reduce(pitResolved, user, new String[] { action }, resolver, clusterService); } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java index 278dc86b7c..a3738dadac 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluator.java @@ -105,7 +105,7 @@ public class PrivilegesEvaluator { private static final WildcardMatcher ACTION_MATCHER = WildcardMatcher.from("indices:data/read/*search*"); private static final Pattern DNFOF_PATTERNS = Pattern.compile( - "indices:(data/read/.*|(admin/(mappings/fields/get.*|shards/search_shards|resolve/index)))" + "indices:(data/read/.*|(admin/(mappings/fields/get.*|shards/search_shards|resolve/index)))" ); private static final IndicesOptions ALLOW_EMPTY = IndicesOptions.fromOptions(true, true, false, false); @@ -135,10 +135,19 @@ public class PrivilegesEvaluator { private DynamicConfigModel dcm; private final NamedXContentRegistry namedXContentRegistry; - public PrivilegesEvaluator(final ClusterService clusterService, final ThreadPool threadPool, - final ConfigurationRepository configurationRepository, final IndexNameExpressionResolver resolver, - AuditLog auditLog, final Settings settings, final PrivilegesInterceptor privilegesInterceptor, final ClusterInfoHolder clusterInfoHolder, - final IndexResolverReplacer irr, boolean dlsFlsEnabled, NamedXContentRegistry namedXContentRegistry) { + public PrivilegesEvaluator( + final ClusterService clusterService, + final ThreadPool threadPool, + final ConfigurationRepository configurationRepository, + final IndexNameExpressionResolver resolver, + AuditLog auditLog, + final Settings settings, + final PrivilegesInterceptor privilegesInterceptor, + final ClusterInfoHolder clusterInfoHolder, + final IndexResolverReplacer irr, + boolean dlsFlsEnabled, + NamedXContentRegistry namedXContentRegistry + ) { super(); this.clusterService = clusterService; @@ -148,9 +157,10 @@ public PrivilegesEvaluator(final ClusterService clusterService, final ThreadPool this.threadContext = threadPool.getThreadContext(); this.privilegesInterceptor = privilegesInterceptor; - - this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean(ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, - ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES); + this.checkSnapshotRestoreWritePrivileges = settings.getAsBoolean( + ConfigConstants.SECURITY_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES, + ConfigConstants.SECURITY_DEFAULT_CHECK_SNAPSHOT_RESTORE_WRITE_PRIVILEGES + ); this.clusterInfoHolder = clusterInfoHolder; this.irr = irr; @@ -178,9 +188,7 @@ private SecurityRoles getSecurityRoles(Set roles) { return configModel.getSecurityRoles().filter(roles); } - public boolean hasRestAdminPermissions(final User user, - final TransportAddress remoteAddress, - final String permissions) { + public boolean hasRestAdminPermissions(final User user, final TransportAddress remoteAddress, final String permissions) { final Set userRoles = mapRoles(user, remoteAddress); return hasRestAdminPermissions(userRoles, permissions); } @@ -191,7 +199,7 @@ private boolean hasRestAdminPermissions(final Set roles, String permissi } public boolean isInitialized() { - return configModel !=null && configModel.getSecurityRoles() != null && dcm != null; + return configModel != null && configModel.getSecurityRoles() != null && dcm != null; } private void setUserInfoInThreadContext(User user, Set mappedRoles) { @@ -208,14 +216,19 @@ private void setUserInfoInThreadContext(User user, Set mappedRoles) { } } - public PrivilegesEvaluatorResponse evaluate(final User user, String action0, final ActionRequest request, - Task task, final Set injectedRoles) { + public PrivilegesEvaluatorResponse evaluate( + final User user, + String action0, + final ActionRequest request, + Task task, + final Set injectedRoles + ) { if (!isInitialized()) { throw new OpenSearchSecurityException("OpenSearch Security is not initialized."); } - if(action0.startsWith("internal:indices/admin/upgrade")) { + if (action0.startsWith("internal:indices/admin/upgrade")) { action0 = "indices:admin/upgrade"; } @@ -231,10 +244,12 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin final TransportAddress caller = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_REMOTE_ADDRESS); Set mappedRoles = (injectedRoles == null) ? mapRoles(user, caller) : injectedRoles; - final String injectedRolesValidationString = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION); - if(injectedRolesValidationString != null) { + final String injectedRolesValidationString = threadContext.getTransient( + ConfigConstants.OPENDISTRO_SECURITY_INJECTED_ROLES_VALIDATION + ); + if (injectedRolesValidationString != null) { HashSet injectedRolesValidationSet = new HashSet<>(Arrays.asList(injectedRolesValidationString.split(","))); - if(!mappedRoles.containsAll(injectedRolesValidationSet)) { + if (!mappedRoles.containsAll(injectedRolesValidationSet)) { presponse.allowed = false; presponse.missingSecurityRoles.addAll(injectedRolesValidationSet); log.info("Roles {} are not mapped to the user {}", injectedRolesValidationSet, user); @@ -257,15 +272,23 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin } if (request instanceof BulkRequest && (Strings.isNullOrEmpty(user.getRequestedTenant()))) { - // Shortcut for bulk actions. The details are checked on the lower level of the BulkShardRequests (Action indices:data/write/bulk[s]). - // This shortcut is only possible if the default tenant is selected, as we might need to rewrite the request for non-default tenants. - // No further access check for the default tenant is necessary, as access will be also checked on the TransportShardBulkAction level. + // Shortcut for bulk actions. The details are checked on the lower level of the BulkShardRequests (Action + // indices:data/write/bulk[s]). + // This shortcut is only possible if the default tenant is selected, as we might need to rewrite the request for non-default + // tenants. + // No further access check for the default tenant is necessary, as access will be also checked on the TransportShardBulkAction + // level. if (!securityRoles.impliesClusterPermissionPermission(action0)) { presponse.missingPrivileges.add(action0); presponse.allowed = false; - log.info("No cluster-level perm match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", user, action0, - securityRoles.getRoleNames(), presponse.missingPrivileges); + log.info( + "No cluster-level perm match for {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + action0, + securityRoles.getRoleNames(), + presponse.missingPrivileges + ); } else { presponse.allowed = true; } @@ -275,7 +298,6 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin final Resolved requestedResolved = irr.resolveRequest(request); presponse.resolved = requestedResolved; - if (isDebugEnabled) { log.debug("RequestedResolved : {}", requestedResolved); } @@ -296,8 +318,7 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin } // check access for point in time requests - if(pitPrivilegesEvaluator.evaluate(request, clusterService, user, securityRoles, - action0, resolver, presponse, irr).isComplete()) { + if (pitPrivilegesEvaluator.evaluate(request, clusterService, user, securityRoles, action0, resolver, presponse, irr).isComplete()) { return presponse; } @@ -308,27 +329,44 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin log.trace("dnfof enabled? {}", dnfofEnabled); } - presponse.evaluatedDlsFlsConfig = getSecurityRoles(mappedRoles).getDlsFls(user, dfmEmptyOverwritesAll, resolver, clusterService, namedXContentRegistry); - + presponse.evaluatedDlsFlsConfig = getSecurityRoles(mappedRoles).getDlsFls( + user, + dfmEmptyOverwritesAll, + resolver, + clusterService, + namedXContentRegistry + ); if (isClusterPerm(action0)) { - if(!securityRoles.impliesClusterPermissionPermission(action0)) { + if (!securityRoles.impliesClusterPermissionPermission(action0)) { presponse.missingPrivileges.add(action0); presponse.allowed = false; - log.info("No cluster-level perm match for {} {} [Action [{}]] [RolesChecked {}]. No permissions for {}", user, requestedResolved, action0, - securityRoles.getRoleNames(), presponse.missingPrivileges); + log.info( + "No cluster-level perm match for {} {} [Action [{}]] [RolesChecked {}]. No permissions for {}", + user, + requestedResolved, + action0, + securityRoles.getRoleNames(), + presponse.missingPrivileges + ); return presponse; } else { - if(request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { + if (request instanceof RestoreSnapshotRequest && checkSnapshotRestoreWritePrivileges) { if (isDebugEnabled) { log.debug("Normally allowed but we need to apply some extra checks for a restore request."); } } else { - if(privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { + if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { - final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex(request, action0, user, dcm, requestedResolved, - mapTenants(user, mappedRoles)); + final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action0, + user, + dcm, + requestedResolved, + mapTenants(user, mappedRoles) + ); if (isDebugEnabled) { log.debug("Result from privileges interceptor for cluster perm: {}", replaceResult); @@ -345,26 +383,28 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin } } - if (dnfofEnabled - && (action0.startsWith("indices:data/read/")) - && !requestedResolved.getAllIndices().isEmpty() - ) { + if (dnfofEnabled && (action0.startsWith("indices:data/read/")) && !requestedResolved.getAllIndices().isEmpty()) { - if(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 + ); - Set reduced = securityRoles.reduce(requestedResolved, user, new String[]{action0}, resolver, clusterService); - - if(reduced.isEmpty()) { + if (reduced.isEmpty()) { presponse.allowed = false; return presponse; } - if(irr.replace(request, true, reduced.toArray(new String[0]))) { + if (irr.replace(request, true, reduced.toArray(new String[0]))) { presponse.missingPrivileges.clear(); presponse.allowed = true; return presponse; @@ -386,7 +426,8 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin } // term aggregations - if (termsAggregationEvaluator.evaluate(requestedResolved, request, clusterService, user, securityRoles, resolver, presponse) .isComplete()) { + if (termsAggregationEvaluator.evaluate(requestedResolved, request, clusterService, user, securityRoles, resolver, presponse) + .isComplete()) { return presponse; } @@ -405,11 +446,18 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin log.debug("Security roles: {}", securityRoles.getRoleNames()); } - //TODO exclude Security index + // TODO exclude Security index - if(privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { + if (privilegesInterceptor.getClass() != PrivilegesInterceptor.class) { - final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex(request, action0, user, dcm, requestedResolved, mapTenants(user, mappedRoles)); + final PrivilegesInterceptor.ReplaceResult replaceResult = privilegesInterceptor.replaceDashboardsIndex( + request, + action0, + user, + dcm, + requestedResolved, + mapTenants(user, mappedRoles) + ); if (isDebugEnabled) { log.debug("Result from privileges interceptor: {}", replaceResult); @@ -428,7 +476,7 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin if (dnfofEnabled && DNFOF_PATTERNS.matcher(action0).matches()) { - if(requestedResolved.getAllIndices().isEmpty()) { + if (requestedResolved.getAllIndices().isEmpty()) { presponse.missingPrivileges.clear(); presponse.allowed = true; return presponse; @@ -436,18 +484,18 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin Set reduced = securityRoles.reduce(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); - if(reduced.isEmpty()) { - if(dcm.isDnfofForEmptyResultsEnabled() && request instanceof IndicesRequest.Replaceable) { + if (reduced.isEmpty()) { + if (dcm.isDnfofForEmptyResultsEnabled() && request instanceof IndicesRequest.Replaceable) { ((IndicesRequest.Replaceable) request).indices(new String[0]); presponse.missingPrivileges.clear(); presponse.allowed = true; - if(request instanceof SearchRequest) { + if (request instanceof SearchRequest) { ((SearchRequest) request).indicesOptions(ALLOW_EMPTY); - } else if(request instanceof ClusterSearchShardsRequest) { + } else if (request instanceof ClusterSearchShardsRequest) { ((ClusterSearchShardsRequest) request).indicesOptions(ALLOW_EMPTY); - } else if(request instanceof GetFieldMappingsRequest) { + } else if (request instanceof GetFieldMappingsRequest) { ((GetFieldMappingsRequest) request).indicesOptions(ALLOW_EMPTY); } @@ -457,16 +505,14 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin return presponse; } - - if(irr.replace(request, true, reduced.toArray(new String[0]))) { + if (irr.replace(request, true, reduced.toArray(new String[0]))) { presponse.missingPrivileges.clear(); presponse.allowed = true; return presponse; } } - - //not bulk, mget, etc request here + // not bulk, mget, etc request here boolean permGiven = false; if (isDebugEnabled) { @@ -475,19 +521,25 @@ public PrivilegesEvaluatorResponse evaluate(final User user, String action0, fin if (dcm.isMultiRolespanEnabled()) { permGiven = securityRoles.impliesTypePermGlobal(requestedResolved, user, allIndexPermsRequiredA, resolver, clusterService); - } else { + } 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.getRoleNames()); + if (!permGiven) { + log.info( + "No {}-level perm match for {} {} [Action [{}]] [RolesChecked {}]", + "index", + user, + requestedResolved, + action0, + securityRoles.getRoleNames() + ); log.info("No permissions for {}", presponse.missingPrivileges); } else { - if(checkFilteredAliases(requestedResolved, action0, isDebugEnabled)) { - presponse.allowed=false; + if (checkFilteredAliases(requestedResolved, action0, isDebugEnabled)) { + presponse.allowed = false; return presponse; } @@ -505,26 +557,21 @@ public Set mapRoles(final User user, final TransportAddress caller) { return this.configModel.mapSecurityRoles(user, caller); } - public Map mapTenants(final User user, Set roles) { return this.configModel.mapTenants(user, roles); } - - public Set getAllConfiguredTenantNames() { return configModel.getAllConfiguredTenantNames(); } public boolean multitenancyEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class - && dcm.isDashboardsMultitenancyEnabled(); + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsMultitenancyEnabled(); } public boolean privateTenantEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class - && dcm.isDashboardsPrivateTenantEnabled(); + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDashboardsPrivateTenantEnabled(); } public String dashboardsDefaultTenant() { @@ -532,8 +579,7 @@ public String dashboardsDefaultTenant() { } public boolean notFailOnForbiddenEnabled() { - return privilegesInterceptor.getClass() != PrivilegesInterceptor.class - && dcm.isDnfofEnabled(); + return privilegesInterceptor.getClass() != PrivilegesInterceptor.class && dcm.isDnfofEnabled(); } public String dashboardsIndex() { @@ -549,10 +595,10 @@ public String dashboardsOpenSearchRole() { } private Set evaluateAdditionalIndexPermissions(final ActionRequest request, final String originalAction) { - //--- check inner bulk requests + // --- check inner bulk requests final Set additionalPermissionsRequired = new HashSet<>(); - if(!isClusterPerm(originalAction)) { + if (!isClusterPerm(originalAction)) { additionalPermissionsRequired.add(originalAction); } @@ -564,18 +610,18 @@ private Set evaluateAdditionalIndexPermissions(final ActionRequest reque 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; + 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; } } } @@ -584,11 +630,11 @@ private Set evaluateAdditionalIndexPermissions(final ActionRequest reque IndicesAliasesRequest bsr = (IndicesAliasesRequest) request; for (AliasActions bir : bsr.getAliasActions()) { switch (bir.actionType()) { - case REMOVE_INDEX: - additionalPermissionsRequired.add(DeleteIndexAction.NAME); - break; - default: - break; + case REMOVE_INDEX: + additionalPermissionsRequired.add(DeleteIndexAction.NAME); + break; + default: + break; } } } @@ -616,17 +662,17 @@ private Set evaluateAdditionalIndexPermissions(final ActionRequest reque } public static boolean isClusterPerm(String action0) { - return ( action0.startsWith("cluster:") - || action0.startsWith("indices:admin/template/") - || action0.startsWith("indices:admin/index_template/") + return (action0.startsWith("cluster:") + || action0.startsWith("indices:admin/template/") + || action0.startsWith("indices:admin/index_template/") || action0.startsWith(SearchScrollAction.NAME) || (action0.equals(BulkAction.NAME)) || (action0.equals(MultiGetAction.NAME)) - || (action0.equals(MultiSearchAction.NAME)) + || (action0.startsWith(MultiSearchAction.NAME)) || (action0.equals(MultiTermVectorsAction.NAME)) || (action0.equals(ReindexAction.NAME)) - ) ; + ); } @SuppressWarnings("unchecked") @@ -667,20 +713,20 @@ public Iterator iterator() { indexMetaDataCollection = indexMetaDataSet; } - //check filtered aliases + // check filtered aliases for (IndexMetadata indexMetaData : indexMetaDataCollection) { final List filteredAliases = new ArrayList(); final Map aliases = indexMetaData.getAliases(); - if(aliases != null && aliases.size() > 0) { + if (aliases != null && aliases.size() > 0) { if (isDebugEnabled) { log.debug("Aliases for {}: {}", indexMetaData.getIndex().getName(), aliases); } final Iterator it = aliases.keySet().iterator(); - while(it.hasNext()) { + while (it.hasNext()) { final String alias = it.next(); final AliasMetadata aliasMetadata = aliases.get(alias); @@ -697,18 +743,21 @@ public Iterator iterator() { } } - if(filteredAliases.size() > 1 && ACTION_MATCHER.test(action)) { - //TODO add queries as dls queries (works only if dls module is installed) - log.error("More than one ({}) filtered alias found for same index ({}). This is currently not supported. Aliases: {}", - filteredAliases.size(), indexMetaData.getIndex().getName(), toString(filteredAliases)); + if (filteredAliases.size() > 1 && ACTION_MATCHER.test(action)) { + // TODO add queries as dls queries (works only if dls module is installed) + log.error( + "More than one ({}) filtered alias found for same index ({}). This is currently not supported. Aliases: {}", + filteredAliases.size(), + indexMetaData.getIndex().getName(), + toString(filteredAliases) + ); return true; } - } //end-for + } // end-for return false; } - private boolean checkDocAllowListHeader(User user, String action, ActionRequest request) { String docAllowListHeader = threadContext.getHeader(ConfigConstants.OPENDISTRO_SECURITY_DOC_ALLOWLIST_HEADER); @@ -741,14 +790,14 @@ private boolean checkDocAllowListHeader(User user, String action, ActionRequest } private List toString(List aliases) { - if(aliases == null || aliases.size() == 0) { + if (aliases == null || aliases.size() == 0) { return Collections.emptyList(); } final List ret = new ArrayList<>(aliases.size()); - for(final AliasMetadata amd: aliases) { - if(amd != null) { + for (final AliasMetadata amd : aliases) { + if (amd != null) { ret.add(amd.alias()); } } diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java index 31ce7095d2..eb082a4a9f 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesEvaluatorResponse.java @@ -44,19 +44,24 @@ public class PrivilegesEvaluatorResponse { CreateIndexRequestBuilder createIndexRequestBuilder; public Resolved getResolved() { - return resolved; - } + return resolved; + } public boolean isAllowed() { return allowed; } + public Set getMissingPrivileges() { return new HashSet(missingPrivileges); } - public Set getMissingSecurityRoles() {return new HashSet<>(missingSecurityRoles); } + public Set getMissingSecurityRoles() { + return new HashSet<>(missingSecurityRoles); + } - public Set getResolvedSecurityRoles() {return new HashSet<>(resolvedSecurityRoles); } + public Set getResolvedSecurityRoles() { + return new HashSet<>(resolvedSecurityRoles); + } public EvaluatedDlsFlsConfig getEvaluatedDlsFlsConfig() { return evaluatedDlsFlsConfig; @@ -86,8 +91,13 @@ public boolean isPending() { @Override public String toString() { - return "PrivEvalResponse [allowed=" + allowed + ", missingPrivileges=" + missingPrivileges + ", evaluatedDlsFlsConfig=" - + evaluatedDlsFlsConfig + "]"; + return "PrivEvalResponse [allowed=" + + allowed + + ", missingPrivileges=" + + missingPrivileges + + ", evaluatedDlsFlsConfig=" + + evaluatedDlsFlsConfig + + "]"; } public static enum PrivilegesEvaluatorResponseState { diff --git a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java index dd569b05fb..f177596573 100644 --- a/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java +++ b/src/main/java/org/opensearch/security/privileges/PrivilegesInterceptor.java @@ -56,6 +56,7 @@ private ReplaceResult(boolean continueEvaluation, boolean accessDenied, CreateIn public static final ReplaceResult CONTINUE_EVALUATION_REPLACE_RESULT = new ReplaceResult(true, false, null); public static final ReplaceResult ACCESS_DENIED_REPLACE_RESULT = new ReplaceResult(false, true, null); public static final ReplaceResult ACCESS_GRANTED_REPLACE_RESULT = new ReplaceResult(false, false, null); + protected static ReplaceResult newAccessGrantedReplaceResult(CreateIndexRequestBuilder createIndexRequestBuilder) { return new ReplaceResult(false, false, createIndexRequestBuilder); } @@ -65,16 +66,26 @@ protected static ReplaceResult newAccessGrantedReplaceResult(CreateIndexRequestB protected final Client client; protected final ThreadPool threadPool; - public PrivilegesInterceptor(final IndexNameExpressionResolver resolver, final ClusterService clusterService, - final Client client, 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 ReplaceResult replaceDashboardsIndex(final ActionRequest request, final String action, final User user, final DynamicConfigModel config, - final Resolved requestedResolved, final Map tenants) { + public ReplaceResult replaceDashboardsIndex( + final ActionRequest request, + final String action, + final User user, + final DynamicConfigModel config, + final Resolved requestedResolved, + final Map tenants + ) { throw new RuntimeException("not implemented"); } diff --git a/src/main/java/org/opensearch/security/privileges/ProtectedIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/ProtectedIndexAccessEvaluator.java index f00f8be73d..e4fd404daa 100644 --- a/src/main/java/org/opensearch/security/privileges/ProtectedIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/ProtectedIndexAccessEvaluator.java @@ -38,11 +38,20 @@ public class ProtectedIndexAccessEvaluator { private final Boolean protectedIndexEnabled; private final WildcardMatcher deniedActionMatcher; - public ProtectedIndexAccessEvaluator(final Settings settings, AuditLog auditLog) { - this.indexMatcher = WildcardMatcher.from(settings.getAsList(ConfigConstants.SECURITY_PROTECTED_INDICES_KEY, ConfigConstants.SECURITY_PROTECTED_INDICES_DEFAULT)); - this.allowedRolesMatcher = WildcardMatcher.from(settings.getAsList(ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_KEY, ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_DEFAULT)); - this.protectedIndexEnabled = settings.getAsBoolean(ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_KEY, ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_DEFAULT); + this.indexMatcher = WildcardMatcher.from( + settings.getAsList(ConfigConstants.SECURITY_PROTECTED_INDICES_KEY, ConfigConstants.SECURITY_PROTECTED_INDICES_DEFAULT) + ); + this.allowedRolesMatcher = WildcardMatcher.from( + settings.getAsList( + ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_KEY, + ConfigConstants.SECURITY_PROTECTED_INDICES_ROLES_DEFAULT + ) + ); + this.protectedIndexEnabled = settings.getAsBoolean( + ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_KEY, + ConfigConstants.SECURITY_PROTECTED_INDICES_ENABLED_DEFAULT + ); this.auditLog = auditLog; final List indexDeniedActionPatterns = new ArrayList(); @@ -58,15 +67,21 @@ public ProtectedIndexAccessEvaluator(final Settings settings, AuditLog auditLog) this.deniedActionMatcher = WildcardMatcher.from(indexDeniedActionPatterns); } - public PrivilegesEvaluatorResponse evaluate(final ActionRequest request, final Task task, final String action, final IndexResolverReplacer.Resolved requestedResolved, - final PrivilegesEvaluatorResponse presponse, final SecurityRoles securityRoles) { + public PrivilegesEvaluatorResponse evaluate( + final ActionRequest request, + final Task task, + final String action, + final IndexResolverReplacer.Resolved requestedResolved, + final PrivilegesEvaluatorResponse presponse, + final SecurityRoles securityRoles + ) { if (!protectedIndexEnabled) { return presponse; } if (!requestedResolved.isLocalAll() - && indexMatcher.matchAny(requestedResolved.getAllIndices()) - && deniedActionMatcher.test(action) - && !allowedRolesMatcher.matchAny(securityRoles.getRoleNames())) { + && indexMatcher.matchAny(requestedResolved.getAllIndices()) + && deniedActionMatcher.test(action) + && !allowedRolesMatcher.matchAny(securityRoles.getRoleNames())) { auditLog.logMissingPrivileges(action, request, task); log.warn("{} for '{}' index/indices is not allowed for a regular user", action, indexMatcher); presponse.allowed = false; @@ -74,26 +89,25 @@ public PrivilegesEvaluatorResponse evaluate(final ActionRequest request, final T } if (requestedResolved.isLocalAll() - && deniedActionMatcher.test(action) - && !allowedRolesMatcher.matchAny(securityRoles.getRoleNames())) { + && deniedActionMatcher.test(action) + && !allowedRolesMatcher.matchAny(securityRoles.getRoleNames())) { auditLog.logMissingPrivileges(action, request, task); log.warn("{} for '_all' indices is not allowed for a regular user", action); presponse.allowed = false; return presponse.markComplete(); } - if((requestedResolved.isLocalAll() - || indexMatcher.matchAny(requestedResolved.getAllIndices())) - && !allowedRolesMatcher.matchAny(securityRoles.getRoleNames())) { + if ((requestedResolved.isLocalAll() || indexMatcher.matchAny(requestedResolved.getAllIndices())) + && !allowedRolesMatcher.matchAny(securityRoles.getRoleNames())) { final boolean isDebugEnabled = log.isDebugEnabled(); - if(request instanceof SearchRequest) { - ((SearchRequest)request).requestCache(Boolean.FALSE); + if (request instanceof SearchRequest) { + ((SearchRequest) request).requestCache(Boolean.FALSE); if (isDebugEnabled) { log.debug("Disable search request cache for this request"); } } - if(request instanceof RealtimeRequest) { + if (request instanceof RealtimeRequest) { ((RealtimeRequest) request).realtime(Boolean.FALSE); if (isDebugEnabled) { log.debug("Disable realtime for this request"); diff --git a/src/main/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluator.java b/src/main/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluator.java index a74ea17ccd..94b0478759 100644 --- a/src/main/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SecurityIndexAccessEvaluator.java @@ -61,14 +61,25 @@ public class SecurityIndexAccessEvaluator { private final boolean systemIndexEnabled; public SecurityIndexAccessEvaluator(final Settings settings, AuditLog auditLog, IndexResolverReplacer irr) { - this.securityIndex = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + this.securityIndex = settings.get( + ConfigConstants.SECURITY_CONFIG_INDEX_NAME, + ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX + ); this.auditLog = auditLog; this.irr = irr; this.filterSecurityIndex = settings.getAsBoolean(ConfigConstants.SECURITY_FILTER_SECURITYINDEX_FROM_ALL_REQUESTS, false); - this.systemIndexMatcher = WildcardMatcher.from(settings.getAsList(ConfigConstants.SECURITY_SYSTEM_INDICES_KEY, ConfigConstants.SECURITY_SYSTEM_INDICES_DEFAULT)); - this.systemIndexEnabled = settings.getAsBoolean(ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY, ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_DEFAULT); - - final boolean restoreSecurityIndexEnabled = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED, false); + this.systemIndexMatcher = WildcardMatcher.from( + settings.getAsList(ConfigConstants.SECURITY_SYSTEM_INDICES_KEY, ConfigConstants.SECURITY_SYSTEM_INDICES_DEFAULT) + ); + this.systemIndexEnabled = settings.getAsBoolean( + ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_KEY, + ConfigConstants.SECURITY_SYSTEM_INDICES_ENABLED_DEFAULT + ); + + final boolean restoreSecurityIndexEnabled = settings.getAsBoolean( + ConfigConstants.SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED, + false + ); final List securityIndexDeniedActionPatternsList = new ArrayList(); securityIndexDeniedActionPatternsList.add("indices:data/write*"); @@ -84,31 +95,44 @@ public SecurityIndexAccessEvaluator(final Settings settings, AuditLog auditLog, securityIndexDeniedActionPatternsListNoSnapshot.add("indices:admin/close*"); securityIndexDeniedActionPatternsListNoSnapshot.add("cluster:admin/snapshot/restore*"); - securityDeniedActionMatcher = WildcardMatcher.from(restoreSecurityIndexEnabled ? securityIndexDeniedActionPatternsList : securityIndexDeniedActionPatternsListNoSnapshot); + securityDeniedActionMatcher = WildcardMatcher.from( + restoreSecurityIndexEnabled ? securityIndexDeniedActionPatternsList : securityIndexDeniedActionPatternsListNoSnapshot + ); } - public PrivilegesEvaluatorResponse evaluate(final ActionRequest request, final Task task, final String action, final Resolved requestedResolved, - final PrivilegesEvaluatorResponse presponse) { + public PrivilegesEvaluatorResponse evaluate( + final ActionRequest request, + final Task task, + final String action, + final Resolved requestedResolved, + final PrivilegesEvaluatorResponse presponse + ) { final boolean isDebugEnabled = log.isDebugEnabled(); if (securityDeniedActionMatcher.test(action)) { - if(requestedResolved.isLocalAll()) { - if(filterSecurityIndex) { - irr.replace(request, false, "*","-"+ securityIndex); + if (requestedResolved.isLocalAll()) { + if (filterSecurityIndex) { + irr.replace(request, false, "*", "-" + securityIndex); if (isDebugEnabled) { - log.debug("Filtered '{}'from {}, resulting list with *,-{} is {}", securityIndex, requestedResolved, securityIndex, irr.resolveRequest(request)); + log.debug( + "Filtered '{}'from {}, resulting list with *,-{} is {}", + securityIndex, + requestedResolved, + securityIndex, + irr.resolveRequest(request) + ); } return presponse; } else { auditLog.logSecurityIndexAttempt(request, action, task); - log.warn( "{} for '_all' indices is not allowed for a regular user", action); + log.warn("{} for '_all' indices is not allowed for a regular user", action); presponse.allowed = false; return presponse.markComplete(); } } else if (matchAnySystemIndices(requestedResolved)) { - if(filterSecurityIndex) { + if (filterSecurityIndex) { Set allWithoutSecurity = new HashSet<>(requestedResolved.getAllIndices()); allWithoutSecurity.remove(securityIndex); - if(allWithoutSecurity.isEmpty()) { + if (allWithoutSecurity.isEmpty()) { if (isDebugEnabled) { log.debug("Filtered '{}' but resulting list is empty", securityIndex); } @@ -130,17 +154,18 @@ public PrivilegesEvaluatorResponse evaluate(final ActionRequest request, final T } } - if(requestedResolved.isLocalAll() || requestedResolved.getAllIndices().contains(securityIndex) - || matchAnySystemIndices(requestedResolved)) { + if (requestedResolved.isLocalAll() + || requestedResolved.getAllIndices().contains(securityIndex) + || matchAnySystemIndices(requestedResolved)) { - if(request instanceof SearchRequest) { - ((SearchRequest)request).requestCache(Boolean.FALSE); + if (request instanceof SearchRequest) { + ((SearchRequest) request).requestCache(Boolean.FALSE); if (isDebugEnabled) { log.debug("Disable search request cache for this request"); } } - if(request instanceof RealtimeRequest) { + if (request instanceof RealtimeRequest) { ((RealtimeRequest) request).realtime(Boolean.FALSE); if (isDebugEnabled) { log.debug("Disable realtime for this request"); @@ -150,12 +175,15 @@ public PrivilegesEvaluatorResponse evaluate(final ActionRequest request, final T return presponse; } - private boolean matchAnySystemIndices(final Resolved requestedResolved){ + private boolean matchAnySystemIndices(final Resolved requestedResolved) { return !getProtectedIndexes(requestedResolved).isEmpty(); } private List getProtectedIndexes(final Resolved requestedResolved) { - final List protectedIndexes = requestedResolved.getAllIndices().stream().filter(securityIndex::equals).collect(Collectors.toList()); + final List protectedIndexes = requestedResolved.getAllIndices() + .stream() + .filter(securityIndex::equals) + .collect(Collectors.toList()); if (systemIndexEnabled) { protectedIndexes.addAll(systemIndexMatcher.getMatchAny(requestedResolved.getAllIndices(), Collectors.toList())); } diff --git a/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java b/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java index c536ae2d2e..23612e1a52 100644 --- a/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/SnapshotRestoreEvaluator.java @@ -49,16 +49,26 @@ public class SnapshotRestoreEvaluator { private final boolean restoreSecurityIndexEnabled; public SnapshotRestoreEvaluator(final Settings settings, AuditLog auditLog) { - this.enableSnapshotRestorePrivilege = settings.getAsBoolean(ConfigConstants.SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE, - ConfigConstants.SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE); + this.enableSnapshotRestorePrivilege = settings.getAsBoolean( + ConfigConstants.SECURITY_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE, + ConfigConstants.SECURITY_DEFAULT_ENABLE_SNAPSHOT_RESTORE_PRIVILEGE + ); this.restoreSecurityIndexEnabled = settings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_RESTORE_SECURITYINDEX_ENABLED, false); - this.securityIndex = settings.get(ConfigConstants.SECURITY_CONFIG_INDEX_NAME, ConfigConstants.OPENDISTRO_SECURITY_DEFAULT_CONFIG_INDEX); + this.securityIndex = settings.get( + ConfigConstants.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) { + public PrivilegesEvaluatorResponse evaluate( + final ActionRequest request, + final Task task, + final String action, + final ClusterInfoHolder clusterInfoHolder, + final PrivilegesEvaluatorResponse presponse + ) { if (!(request instanceof RestoreSnapshotRequest)) { return presponse; @@ -78,7 +88,6 @@ public PrivilegesEvaluatorResponse evaluate(final ActionRequest request, final T return presponse; } - if (clusterInfoHolder.isLocalNodeElectedClusterManager() == Boolean.FALSE) { presponse.allowed = true; return presponse.markComplete(); diff --git a/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java b/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java index 53709458fd..d06a45726a 100644 --- a/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java +++ b/src/main/java/org/opensearch/security/privileges/TermsAggregationEvaluator.java @@ -48,55 +48,65 @@ 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 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 Resolved resolved, final ActionRequest request, ClusterService clusterService, User user, SecurityRoles securityRoles, IndexNameExpressionResolver resolver, PrivilegesEvaluatorResponse presponse) { + public TermsAggregationEvaluator() {} + + public PrivilegesEvaluatorResponse evaluate( + final Resolved resolved, + final ActionRequest request, + ClusterService clusterService, + User user, + SecurityRoles securityRoles, + IndexNameExpressionResolver resolver, + PrivilegesEvaluatorResponse presponse + ) { try { - if(request instanceof SearchRequest) { + 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.getAllPermittedIndicesForDashboards(resolved, 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(); - } - } + 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.getAllPermittedIndicesForDashboards( + resolved, + 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); + log.warn("Unable to evaluate terms aggregation", e); return presponse; } diff --git a/src/main/java/org/opensearch/security/queries/QueryBuilderTraverser.java b/src/main/java/org/opensearch/security/queries/QueryBuilderTraverser.java index 7bde8523a5..61cc969fdb 100644 --- a/src/main/java/org/opensearch/security/queries/QueryBuilderTraverser.java +++ b/src/main/java/org/opensearch/security/queries/QueryBuilderTraverser.java @@ -65,7 +65,11 @@ public static Set findAll(QueryBuilder queryBuilder, Predicate patterns = requestedPatterns==null?null:Arrays.asList(requestedPatterns); + final List patterns = requestedPatterns == null ? null : Arrays.asList(requestedPatterns); - if(IndexNameExpressionResolver.isAllIndices(patterns)) { + if (IndexNameExpressionResolver.isAllIndices(patterns)) { return true; } - if(patterns.size() == 1 && patterns.contains("*")) { + if (patterns.size() == 1 && patterns.contains("*")) { return true; } - if(new HashSet(patterns).equals(NULL_SET)) { + if (new HashSet(patterns).equals(NULL_SET)) { return true; } @@ -136,15 +136,15 @@ private static final boolean isLocalAll(String... requestedPatterns) { } private static final boolean isLocalAll(Collection patterns) { - if(IndexNameExpressionResolver.isAllIndices(patterns)) { + if (IndexNameExpressionResolver.isAllIndices(patterns)) { return true; } - if(patterns.contains("_all")) { + if (patterns.contains("_all")) { return true; } - if(new HashSet(patterns).equals(NULL_SET)) { + if (new HashSet(patterns).equals(NULL_SET)) { return true; } @@ -170,10 +170,15 @@ private class ResolvedIndicesProvider implements IndicesProvider { name = request.getClass().getSimpleName(); } - private void resolveIndexPatterns(final String name, final IndicesOptions indicesOptions, final boolean enableCrossClusterResolution, final String[] original) { + private void resolveIndexPatterns( + final String name, + final IndicesOptions indicesOptions, + final boolean enableCrossClusterResolution, + final String[] original + ) { final boolean isTraceEnabled = log.isTraceEnabled(); if (isTraceEnabled) { - log.trace("resolve requestedPatterns: "+ Arrays.toString(original)); + log.trace("resolve requestedPatterns: " + Arrays.toString(original)); } if (isAllWithNoRemote(original)) { @@ -189,14 +194,16 @@ private void resolveIndexPatterns(final String name, final IndicesOptions indice final RemoteClusterService remoteClusterService = OpenSearchSecurityPlugin.GuiceHolder.getRemoteClusterService(); - if(remoteClusterService.isCrossClusterSearchEnabled() && enableCrossClusterResolution) { + if (remoteClusterService.isCrossClusterSearchEnabled() && enableCrossClusterResolution) { remoteIndices = new HashSet<>(); final Map remoteClusterIndices = OpenSearchSecurityPlugin.GuiceHolder.getRemoteClusterService() - .groupIndices(indicesOptions, original, idx -> resolver.hasIndexAbstraction(idx, clusterService.state())); - final Set remoteClusters = remoteClusterIndices.keySet().stream() - .filter(k->!RemoteClusterService.LOCAL_CLUSTER_GROUP_KEY.equals(k)).collect(Collectors.toSet()); - for(String remoteCluster : remoteClusters) { - for(String remoteIndex : remoteClusterIndices.get(remoteCluster).indices()) { + .groupIndices(indicesOptions, original, idx -> resolver.hasIndexAbstraction(idx, clusterService.state())); + final Set remoteClusters = remoteClusterIndices.keySet() + .stream() + .filter(k -> !RemoteClusterService.LOCAL_CLUSTER_GROUP_KEY.equals(k)) + .collect(Collectors.toSet()); + for (String remoteCluster : remoteClusters) { + for (String remoteIndex : remoteClusterIndices.get(remoteCluster).indices()) { remoteIndices.add(RemoteClusterService.buildRemoteIndexName(remoteCluster, remoteIndex)); } } @@ -211,7 +218,12 @@ private void resolveIndexPatterns(final String name, final IndicesOptions indice } if (isTraceEnabled) { - log.trace("CCS is enabled, we found this local patterns " + localRequestedPatterns + " and this remote patterns: " + remoteIndices); + log.trace( + "CCS is enabled, we found this local patterns " + + localRequestedPatterns + + " and this remote patterns: " + + remoteIndices + ); } } else { @@ -239,28 +251,33 @@ private void resolveIndexPatterns(final String name, final IndicesOptions indice else { final ClusterState state = clusterService.state(); - final Set dateResolvedLocalRequestedPatterns = localRequestedPatterns - .stream() - .map(resolver::resolveDateMathExpression) - .collect(Collectors.toSet()); + final Set dateResolvedLocalRequestedPatterns = localRequestedPatterns.stream() + .map(resolver::resolveDateMathExpression) + .collect(Collectors.toSet()); final WildcardMatcher dateResolvedMatcher = WildcardMatcher.from(dateResolvedLocalRequestedPatterns); - //fill matchingAliases + // fill matchingAliases final Map lookup = state.metadata().getIndicesLookup(); matchingAliases = lookup.entrySet() - .stream() - .filter(e -> e.getValue().getType() == ALIAS) - .map(Map.Entry::getKey) - .filter(dateResolvedMatcher) - .collect(Collectors.toSet()); + .stream() + .filter(e -> e.getValue().getType() == ALIAS) + .map(Map.Entry::getKey) + .filter(dateResolvedMatcher) + .collect(Collectors.toSet()); final boolean isDebugEnabled = log.isDebugEnabled(); try { - matchingAllIndices = Arrays.asList(resolver.concreteIndexNames(state, indicesOptions, localRequestedPatterns.toArray(new String[0]))); + matchingAllIndices = Arrays.asList( + resolver.concreteIndexNames(state, indicesOptions, localRequestedPatterns.toArray(new String[0])) + ); matchingDataStreams = resolver.dataStreamNames(state, indicesOptions, localRequestedPatterns.toArray(new String[0])); if (isDebugEnabled) { - log.debug("Resolved pattern {} to indices: {} and data-streams: {}", - localRequestedPatterns, matchingAllIndices, matchingDataStreams); + log.debug( + "Resolved pattern {} to indices: {} and data-streams: {}", + localRequestedPatterns, + matchingAllIndices, + matchingDataStreams + ); } } catch (IndexNotFoundException e1) { if (isDebugEnabled) { @@ -276,8 +293,17 @@ private void resolveIndexPatterns(final String name, final IndicesOptions indice } if (isTraceEnabled) { - log.trace("Resolved patterns {} for {} ({}) to [aliases {}, allIndices {}, dataStreams {}, originalRequested{}, remote indices {}]", - original, name, this.name, matchingAliases, matchingAllIndices, matchingDataStreams, Arrays.toString(original), remoteIndices); + log.trace( + "Resolved patterns {} for {} ({}) to [aliases {}, allIndices {}, dataStreams {}, originalRequested{}, remote indices {}]", + original, + name, + this.name, + matchingAliases, + matchingAllIndices, + matchingDataStreams, + Arrays.toString(original), + remoteIndices + ); } resolveTo(matchingAliases, matchingAllIndices, matchingDataStreams, original, remoteIndices); @@ -289,8 +315,13 @@ private void resolveToLocalAll() { originalRequested.add(Resolved.ANY); } - private void resolveTo(Iterable matchingAliases, Iterable matchingAllIndices, - Iterable matchingDataStreams, String[] original, Iterable remoteIndices) { + private void resolveTo( + Iterable matchingAliases, + Iterable matchingAllIndices, + Iterable matchingDataStreams, + String[] original, + Iterable remoteIndices + ) { aliases.addAll(matchingAliases); allIndices.addAll(matchingAllIndices); allIndices.addAll(matchingDataStreams); @@ -302,21 +333,23 @@ private void resolveTo(Iterable matchingAliases, Iterable matchi public String[] provide(String[] original, Object localRequest, boolean supportsReplace) { final IndicesOptions indicesOptions = indicesOptionsFrom(localRequest); final boolean enableCrossClusterResolution = localRequest instanceof FieldCapabilitiesRequest - || localRequest instanceof SearchRequest - || localRequest instanceof ResolveIndexAction.Request; + || localRequest instanceof SearchRequest + || localRequest instanceof ResolveIndexAction.Request; // skip the whole thing if we have seen this exact resolveIndexPatterns request - if (alreadyResolved.add(new MultiKey(indicesOptions, enableCrossClusterResolution, - (original != null) ? new MultiKey(original, false) : null))) { + if (alreadyResolved.add( + new MultiKey(indicesOptions, enableCrossClusterResolution, (original != null) ? new MultiKey(original, false) : null) + )) { resolveIndexPatterns(localRequest.getClass().getSimpleName(), indicesOptions, enableCrossClusterResolution, original); } return IndicesProvider.NOOP; } Resolved resolved(IndicesOptions indicesOptions) { - final Resolved resolved = alreadyResolved.isEmpty() ? Resolved._LOCAL_ALL : - new Resolved(aliases.build(), allIndices.build(), originalRequested.build(), remoteIndices.build(), indicesOptions); + final Resolved resolved = alreadyResolved.isEmpty() + ? Resolved._LOCAL_ALL + : new Resolved(aliases.build(), allIndices.build(), originalRequested.build(), remoteIndices.build(), indicesOptions); - if(log.isTraceEnabled()) { + if (log.isTraceEnabled()) { log.trace("Finally resolved for {}: {}", name, resolved); } @@ -324,16 +357,17 @@ Resolved resolved(IndicesOptions indicesOptions) { } } - //dnfof + // 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 && !isAllWithNoRemote(original)) { + if (supportsReplace) { + if (retainMode && !isAllWithNoRemote(original)) { final Resolved resolved = resolveRequest(request); - final List retained = WildcardMatcher.from(resolved.getAllIndices()).getMatchAny(replacements, Collectors.toList()); + final List retained = WildcardMatcher.from(resolved.getAllIndices()) + .getMatchAny(replacements, Collectors.toList()); retained.addAll(resolved.getRemoteIndices()); return retained.toArray(new String[0]); } @@ -361,7 +395,13 @@ public final static class Resolved { private static final String ANY = "*"; private static final ImmutableSet All_SET = ImmutableSet.of(ANY); private static final Set types = All_SET; - public static final Resolved _LOCAL_ALL = new Resolved(All_SET, All_SET, All_SET, ImmutableSet.of(), SearchRequest.DEFAULT_INDICES_OPTIONS); + public static final Resolved _LOCAL_ALL = new Resolved( + All_SET, + All_SET, + All_SET, + ImmutableSet.of(), + SearchRequest.DEFAULT_INDICES_OPTIONS + ); private final Set aliases; private final Set allIndices; @@ -370,16 +410,19 @@ public final static class Resolved { private final boolean isLocalAll; private final IndicesOptions indicesOptions; - public Resolved(final ImmutableSet aliases, - final ImmutableSet allIndices, - final ImmutableSet originalRequested, - final ImmutableSet remoteIndices, - IndicesOptions indicesOptions) { + public Resolved( + final ImmutableSet aliases, + final ImmutableSet allIndices, + final ImmutableSet originalRequested, + final ImmutableSet remoteIndices, + IndicesOptions indicesOptions + ) { this.aliases = aliases; this.allIndices = allIndices; this.originalRequested = originalRequested; this.remoteIndices = remoteIndices; - this.isLocalAll = IndexResolverReplacer.isLocalAll(originalRequested.toArray(new String[0])) || (aliases.contains("*") && allIndices.contains("*")); + this.isLocalAll = IndexResolverReplacer.isLocalAll(originalRequested.toArray(new String[0])) + || (aliases.contains("*") && allIndices.contains("*")); this.indicesOptions = indicesOptions; } @@ -417,8 +460,17 @@ public Set getRemoteIndices() { @Override public String toString() { - return "Resolved [aliases=" + aliases + ", allIndices=" + allIndices + ", types=" + types - + ", originalRequested=" + originalRequested + ", remoteIndices=" + remoteIndices + "]"; + return "Resolved [aliases=" + + aliases + + ", allIndices=" + + allIndices + + ", types=" + + types + + ", originalRequested=" + + originalRequested + + ", remoteIndices=" + + remoteIndices + + "]"; } @Override @@ -434,33 +486,22 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; + 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 (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 (other.allIndices != null) return false; + } else if (!allIndices.equals(other.allIndices)) return false; if (originalRequested == null) { - if (other.originalRequested != null) - return false; - } else if (!originalRequested.equals(other.originalRequested)) - return false; + if (other.originalRequested != null) return false; + } else if (!originalRequested.equals(other.originalRequested)) return false; if (remoteIndices == null) { - if (other.remoteIndices != null) - return false; - } else if (!remoteIndices.equals(other.remoteIndices)) - return false; + if (other.remoteIndices != null) return false; + } else if (!remoteIndices.equals(other.remoteIndices)) return false; return true; } } @@ -482,42 +523,42 @@ private List renamedIndices(final RestoreSnapshotRequest request, final } } - - //-- + // -- @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) { + if (indices == IndicesProvider.NOOP) { return false; } final boolean isTraceEnabled = log.isTraceEnabled(); - if(!allowEmpty && (indices == null || indices.length == 0)) { - if(isTraceEnabled && request != null) { - log.trace("Null or empty indices for "+request.getClass().getName()); + if (!allowEmpty && (indices == null || indices.length == 0)) { + if (isTraceEnabled && request != null) { + log.trace("Null or empty indices for " + request.getClass().getName()); } return false; } - if(!allowEmpty && needsToBeSizeOne && indices.length != 1) { - if(isTraceEnabled && request != null) { - log.trace("To much indices for "+request.getClass().getName()); + if (!allowEmpty && needsToBeSizeOne && indices.length != 1) { + if (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(isTraceEnabled && request != null) { - log.trace("At least one null or empty index for "+request.getClass().getName()); + if (index == null || index.isEmpty()) { + // not allowed + if (isTraceEnabled && request != null) { + log.trace("At least one null or empty index for " + request.getClass().getName()); } return false; } @@ -537,7 +578,7 @@ private boolean getOrReplaceAllIndices(final Object request, final IndicesProvid final boolean isDebugEnabled = log.isDebugEnabled(); final boolean isTraceEnabled = log.isTraceEnabled(); if (isTraceEnabled) { - log.trace("getOrReplaceAllIndices() for "+request.getClass()); + log.trace("getOrReplaceAllIndices() for " + request.getClass()); } boolean result = true; @@ -550,7 +591,7 @@ private boolean getOrReplaceAllIndices(final Object request, final IndicesProvid } else if (request instanceof MultiGetRequest) { - for (ListIterator it = ((MultiGetRequest) request).getItems().listIterator(); it.hasNext();){ + 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) { @@ -574,12 +615,12 @@ private boolean getOrReplaceAllIndices(final Object request, final IndicesProvid result = getOrReplaceAllIndices(ar, provider, false) && result; } - } else if(request instanceof PutMappingRequest) { + } 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) { + 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; } @@ -587,58 +628,64 @@ private boolean getOrReplaceAllIndices(final Object request, final IndicesProvid ((PutMappingRequest) request).setConcreteIndex(null); } else { String[] newIndices = provider.provide(((PutMappingRequest) request).indices(), request, true); - if(checkIndices(request, newIndices, false, allowEmptyIndices) == false) { + if (checkIndices(request, newIndices, false, allowEmptyIndices) == false) { return false; } ((PutMappingRequest) request).indices(newIndices); } - } else if(request instanceof RestoreSnapshotRequest) { + } else if (request instanceof RestoreSnapshotRequest) { - if(clusterInfoHolder.isLocalNodeElectedClusterManager() == Boolean.FALSE) { - return true; - } + if (clusterInfoHolder.isLocalNodeElectedClusterManager() == Boolean.FALSE) { + return true; + } - final RestoreSnapshotRequest restoreRequest = (RestoreSnapshotRequest) request; - final SnapshotInfo snapshotInfo = SnapshotRestoreHelper.getSnapshotInfo(restoreRequest); + 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 (isDebugEnabled) { - log.debug("snapshot: {} contains this indices: {}", snapshotInfo.snapshotId().getName(), renamedTargetIndices); - } - provider.provide(renamedTargetIndices.toArray(new String[0]), request, false); + 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 (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()) { + 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) { + if (checkIndices(request, newIndices, true, allowEmptyIndices) == false) { return false; } - ((DeleteRequest) request).index(newIndices.length!=1?null:newIndices[0]); + ((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) { + if (checkIndices(request, newIndices, true, allowEmptyIndices) == false) { return false; } - ((UpdateRequest) request).index(newIndices.length!=1?null:newIndices[0]); + ((UpdateRequest) request).index(newIndices.length != 1 ? null : newIndices[0]); } else if (request instanceof SingleShardRequest) { final SingleShardRequest singleShardRequest = (SingleShardRequest) request; final String index = singleShardRequest.index(); - String[] indices = provider.provide(index == null ? null : new String[]{index}, request, true); + String[] indices = provider.provide(index == null ? null : new String[] { index }, request, true); if (!checkIndices(request, indices, true, allowEmptyIndices)) { return false; } - singleShardRequest.index(indices.length != 1? null : indices[0]); + singleShardRequest.index(indices.length != 1 ? null : indices[0]); } else if (request instanceof FieldCapabilitiesIndexRequest) { // FieldCapabilitiesIndexRequest does not support replacing the indexes. // However, the indexes are always determined by FieldCapabilitiesRequest which will be reduced below @@ -648,56 +695,56 @@ private boolean getOrReplaceAllIndices(final Object request, final IndicesProvid String index = fieldCapabilitiesRequest.index(); - String[] newIndices = provider.provide(new String[]{index}, request, true); + String[] newIndices = provider.provide(new String[] { index }, request, true); if (!checkIndices(request, newIndices, true, allowEmptyIndices)) { return false; } } else if (request instanceof IndexRequest) { String[] newIndices = provider.provide(((IndexRequest) request).indices(), request, true); - if(checkIndices(request, newIndices, true, allowEmptyIndices) == false) { + if (checkIndices(request, newIndices, true, allowEmptyIndices) == false) { return false; } - ((IndexRequest) request).index(newIndices.length!=1?null:newIndices[0]); + ((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) { + 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? + // replace not supported? } else if (request instanceof ReplicationRequest) { String[] newIndices = provider.provide(((ReplicationRequest) request).indices(), request, true); - if(checkIndices(request, newIndices, true, allowEmptyIndices) == false) { + if (checkIndices(request, newIndices, true, allowEmptyIndices) == false) { return false; } - ((ReplicationRequest) request).index(newIndices.length!=1?null:newIndices[0]); + ((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) { + if (checkIndices(request, newIndices, true, allowEmptyIndices) == false) { return false; } - ((MultiGetRequest.Item) request).index(newIndices.length!=1?null:newIndices[0]); + ((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) { + if (checkIndices(request, newIndices, true, allowEmptyIndices) == false) { return false; } - ((CreateIndexRequest) request).index(newIndices.length!=1?null:newIndices[0]); + ((CreateIndexRequest) request).index(newIndices.length != 1 ? null : newIndices[0]); } else if (request instanceof CreateDataStreamAction.Request) { provider.provide(((CreateDataStreamAction.Request) request).indices(), request, false); } 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 + // do nothing } else if (request instanceof MainRequest) { - //do nothing + // do nothing } else if (request instanceof ClearScrollRequest) { - //do nothing + // do nothing } else if (request instanceof SearchScrollRequest) { - //do nothing + // do nothing } else if (request instanceof PutComponentTemplateAction.Request) { // do nothing } else { @@ -712,17 +759,15 @@ private boolean getOrReplaceAllIndices(final Object request, final IndicesProvid private IndicesOptions indicesOptionsFrom(Object localRequest) { - if(!respectRequestIndicesOptions) { + if (!respectRequestIndicesOptions) { return IndicesOptions.fromOptions(false, true, true, false, true); } if (IndicesRequest.class.isInstance(localRequest)) { return ((IndicesRequest) localRequest).indicesOptions(); - } - else if (RestoreSnapshotRequest.class.isInstance(localRequest)) { + } else if (RestoreSnapshotRequest.class.isInstance(localRequest)) { return ((RestoreSnapshotRequest) localRequest).indicesOptions(); - } - else { + } else { return IndicesOptions.fromOptions(false, true, true, false, true); } } diff --git a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java index a7620f6bdc..352d99b57e 100644 --- a/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/DashboardsInfoAction.java @@ -55,25 +55,22 @@ public class DashboardsInfoAction extends BaseRestHandler { private static final List routes = ImmutableList.builder() - .addAll(addRoutesPrefix( - ImmutableList.of( - new Route(GET, "/dashboardsinfo"), - new Route(POST, "/dashboardsinfo") - ), - "/_plugins/_security")) - .addAll(addRoutesPrefix( - ImmutableList.of( - new Route(GET, "/kibanainfo"), - new Route(POST, "/kibanainfo") - ), - "/_opendistro/_security")) + .addAll( + addRoutesPrefix(ImmutableList.of(new Route(GET, "/dashboardsinfo"), new Route(POST, "/dashboardsinfo")), "/_plugins/_security") + ) + .addAll(addRoutesPrefix(ImmutableList.of(new Route(GET, "/kibanainfo"), new Route(POST, "/kibanainfo")), "/_opendistro/_security")) .build(); private final Logger log = LogManager.getLogger(this.getClass()); private final PrivilegesEvaluator evaluator; private final ThreadContext threadContext; - public DashboardsInfoAction(final Settings settings, final RestController controller, final PrivilegesEvaluator evaluator, final ThreadPool threadPool) { + public DashboardsInfoAction( + final Settings settings, + final RestController controller, + final PrivilegesEvaluator evaluator, + final ThreadPool threadPool + ) { super(); this.threadContext = threadPool.getThreadContext(); this.evaluator = evaluator; @@ -90,15 +87,15 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli @Override public void accept(RestChannel channel) throws Exception { - XContentBuilder builder = channel.newBuilder(); //NOSONAR + XContentBuilder builder = channel.newBuilder(); // NOSONAR BytesRestResponse response = null; try { - final User user = (User)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final User user = (User) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); builder.startObject(); - builder.field("user_name", user==null?null:user.getName()); + builder.field("user_name", user == null ? null : user.getName()); builder.field("not_fail_on_forbidden_enabled", evaluator.notFailOnForbiddenEnabled()); builder.field("opensearch_dashboards_mt_enabled", evaluator.multitenancyEnabled()); builder.field("opensearch_dashboards_index", evaluator.dashboardsIndex()); @@ -111,13 +108,13 @@ public void accept(RestChannel channel) throws Exception { response = new BytesRestResponse(RestStatus.OK, builder); } catch (final Exception e1) { log.error(e1.toString()); - builder = channel.newBuilder(); //NOSONAR + 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) { + if (builder != null) { builder.close(); } } @@ -132,5 +129,4 @@ public String getName() { return "Kibana Info Action"; } - } diff --git a/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java b/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java index b328a049c1..379a3b6b13 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityConfigUpdateAction.java @@ -40,9 +40,7 @@ public class SecurityConfigUpdateAction extends BaseRestHandler { - private static final List routes = addRoutesPrefix(ImmutableList.of( - new Route(PUT, "/configupdate")), - "/_plugins/_security"); + private static final List routes = addRoutesPrefix(ImmutableList.of(new Route(PUT, "/configupdate")), "/_plugins/_security"); private final ThreadContext threadContext; private final AdminDNs adminDns; @@ -50,8 +48,14 @@ public class SecurityConfigUpdateAction extends BaseRestHandler { private final Path configPath; private final PrincipalExtractor principalExtractor; - public SecurityConfigUpdateAction(final Settings settings, final RestController controller, final ThreadPool threadPool, final AdminDNs adminDns, - Path configPath, PrincipalExtractor principalExtractor) { + public SecurityConfigUpdateAction( + final Settings settings, + final RestController controller, + final ThreadPool threadPool, + final AdminDNs adminDns, + Path configPath, + PrincipalExtractor principalExtractor + ) { super(); this.threadContext = threadPool.getThreadContext(); this.adminDns = adminDns; @@ -60,11 +64,13 @@ public SecurityConfigUpdateAction(final Settings settings, final RestController this.principalExtractor = principalExtractor; } - @Override public List routes() { + @Override + public List routes() { return routes; } - @Override protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { + @Override + protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { String[] configTypes = request.paramAsStringArrayOrEmptyIfAll("config_types"); SSLRequestHelper.SSLInfo sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, request, principalExtractor); @@ -75,7 +81,7 @@ public SecurityConfigUpdateAction(final Settings settings, final RestController final User user = threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - //only allowed for admins + // only allowed for admins if (user == null || !adminDns.isAdmin(user)) { return channel -> channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, "")); } else { @@ -86,7 +92,8 @@ public SecurityConfigUpdateAction(final Settings settings, final RestController } } - @Override public String getName() { + @Override + public String getName() { return "Security config update"; } diff --git a/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java b/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java index 17d5ee122f..0631e3044a 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityHealthAction.java @@ -47,10 +47,11 @@ import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class SecurityHealthAction extends BaseRestHandler { - private static final List routes = addRoutesPrefix(ImmutableList.of( - new Route(GET, "/health"), - new Route(POST, "/health") - ), "/_opendistro/_security", "/_plugins/_security"); + private static final List routes = addRoutesPrefix( + ImmutableList.of(new Route(GET, "/health"), new Route(POST, "/health")), + "/_opendistro/_security", + "/_plugins/_security" + ); private final BackendRegistry registry; @@ -68,7 +69,7 @@ public List routes() { protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { return new RestChannelConsumer() { - final String mode = request.param("mode","strict"); + final String mode = request.param("mode", "strict"); @Override public void accept(RestChannel channel) throws Exception { @@ -77,7 +78,6 @@ public void accept(RestChannel channel) throws Exception { BytesRestResponse response = null; try { - String status = "UP"; String message = null; @@ -99,11 +99,9 @@ public void accept(RestChannel channel) throws Exception { builder.close(); } - channel.sendResponse(response); } - }; } diff --git a/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java b/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java index 7867e8790d..6159f555c7 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityInfoAction.java @@ -60,16 +60,22 @@ import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; public class SecurityInfoAction extends BaseRestHandler { - private static final List routes = addRoutesPrefix(ImmutableList.of( - new Route(GET, "/authinfo"), - new Route(POST, "/authinfo") - ),"/_opendistro/_security", "/_plugins/_security"); + private static final List routes = addRoutesPrefix( + ImmutableList.of(new Route(GET, "/authinfo"), new Route(POST, "/authinfo")), + "/_opendistro/_security", + "/_plugins/_security" + ); private final Logger log = LogManager.getLogger(this.getClass()); private final PrivilegesEvaluator evaluator; private final ThreadContext threadContext; - public SecurityInfoAction(final Settings settings, final RestController controller, final PrivilegesEvaluator evaluator, final ThreadPool threadPool) { + public SecurityInfoAction( + final Settings settings, + final RestController controller, + final PrivilegesEvaluator evaluator, + final ThreadPool threadPool + ) { super(); this.threadContext = threadPool.getThreadContext(); this.evaluator = evaluator; @@ -86,12 +92,11 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli @Override public void accept(RestChannel channel) throws Exception { - XContentBuilder builder = channel.newBuilder(); //NOSONAR + 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); @@ -101,41 +106,54 @@ public void accept(RestChannel channel) throws Exception { final Set securityRoles = evaluator.mapRoles(user, remoteAddress); 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("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("backend_roles", user == null ? null : user.getRoles()); + builder.field("custom_attribute_names", user == null ? null : user.getCustomAttributesMap().keySet()); builder.field("roles", securityRoles); builder.field("tenants", evaluator.mapTenants(user, securityRoles)); - builder.field("principal", (String)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_SSL_PRINCIPAL)); + 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)); + builder.field("sso_logout_url", (String) threadContext.getTransient(ConfigConstants.SSO_LOGOUT_URL)); - if(user != null && verbose) { + 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)); + 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 + // ignore } } - builder.endObject(); response = new BytesRestResponse(RestStatus.OK, builder); } catch (final Exception e1) { - log.error(e1.toString(),e1); - builder = channel.newBuilder(); //NOSONAR + 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) { + if (builder != null) { builder.close(); } } diff --git a/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java b/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java index 982448a53f..8f20a0b9a2 100644 --- a/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java +++ b/src/main/java/org/opensearch/security/rest/SecurityWhoAmIAction.java @@ -41,88 +41,93 @@ import static org.opensearch.rest.RestRequest.Method.POST; import static org.opensearch.security.dlic.rest.support.Utils.addRoutesPrefix; - public class SecurityWhoAmIAction extends BaseRestHandler { - private static final List routes = addRoutesPrefix(ImmutableList.of( - new Route(GET, "/whoami"), - new Route(POST, "/whoami")), - "/_plugins/_security"); - - private final Logger log = LogManager.getLogger(this.getClass()); - private final AdminDNs adminDns; - private final Settings settings; - private final Path configPath; - private final PrincipalExtractor principalExtractor; - private final List nodesDn ; - - public SecurityWhoAmIAction(final Settings settings, final RestController controller, - final ThreadPool threadPool, final AdminDNs adminDns, Path configPath, PrincipalExtractor principalExtractor) { - super(); - this.adminDns = adminDns; - this.settings = settings; - this.configPath = configPath; - this.principalExtractor = principalExtractor; - - nodesDn = settings.getAsList(ConfigConstants.SECURITY_NODES_DN, Collections.emptyList()); - } - - @Override - public List routes() { - return routes; - } - - @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(); - BytesRestResponse response = null; - - try { - - SSLInfo sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, request, principalExtractor); - - if(sslInfo == null) { - response = new BytesRestResponse(RestStatus.FORBIDDEN, "No security data"); - } else { - - final String dn = sslInfo.getPrincipal(); - final boolean isAdmin = adminDns.isAdminDN(dn); - final boolean isNodeCertificateRequest = dn != null && WildcardMatcher.from(nodesDn, true).matchAny(dn); - - builder.startObject(); - builder.field("dn", dn); - builder.field("is_admin", isAdmin); - builder.field("is_node_certificate_request", isNodeCertificateRequest); - builder.endObject(); - - response = new BytesRestResponse(RestStatus.OK, builder); - - } - } catch (final Exception e1) { - log.error(e1.toString(), e1); - builder = channel.newBuilder(); - 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 "Security Plugin Who am i"; - } + private static final List routes = addRoutesPrefix( + ImmutableList.of(new Route(GET, "/whoami"), new Route(POST, "/whoami")), + "/_plugins/_security" + ); + + private final Logger log = LogManager.getLogger(this.getClass()); + private final AdminDNs adminDns; + private final Settings settings; + private final Path configPath; + private final PrincipalExtractor principalExtractor; + private final List nodesDn; + + public SecurityWhoAmIAction( + final Settings settings, + final RestController controller, + final ThreadPool threadPool, + final AdminDNs adminDns, + Path configPath, + PrincipalExtractor principalExtractor + ) { + super(); + this.adminDns = adminDns; + this.settings = settings; + this.configPath = configPath; + this.principalExtractor = principalExtractor; + + nodesDn = settings.getAsList(ConfigConstants.SECURITY_NODES_DN, Collections.emptyList()); + } + + @Override + public List routes() { + return routes; + } + + @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(); + BytesRestResponse response = null; + + try { + + SSLInfo sslInfo = SSLRequestHelper.getSSLInfo(settings, configPath, request, principalExtractor); + + if (sslInfo == null) { + response = new BytesRestResponse(RestStatus.FORBIDDEN, "No security data"); + } else { + + final String dn = sslInfo.getPrincipal(); + final boolean isAdmin = adminDns.isAdminDN(dn); + final boolean isNodeCertificateRequest = dn != null && WildcardMatcher.from(nodesDn, true).matchAny(dn); + + builder.startObject(); + builder.field("dn", dn); + builder.field("is_admin", isAdmin); + builder.field("is_node_certificate_request", isNodeCertificateRequest); + builder.endObject(); + + response = new BytesRestResponse(RestStatus.OK, builder); + + } + } catch (final Exception e1) { + log.error(e1.toString(), e1); + builder = channel.newBuilder(); + 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 "Security Plugin Who am i"; + } } diff --git a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java index f7b2a606c6..c6f09efd98 100644 --- a/src/main/java/org/opensearch/security/rest/TenantInfoAction.java +++ b/src/main/java/org/opensearch/security/rest/TenantInfoAction.java @@ -65,11 +65,10 @@ public class TenantInfoAction extends BaseRestHandler { private static final List routes = addRoutesPrefix( - ImmutableList.of( - new Route(GET, "/tenantinfo"), - new Route(POST, "/tenantinfo") - ), - "/_opendistro/_security", "/_plugins/_security"); + ImmutableList.of(new Route(GET, "/tenantinfo"), new Route(POST, "/tenantinfo")), + "/_opendistro/_security", + "/_plugins/_security" + ); private final Logger log = LogManager.getLogger(this.getClass()); private final PrivilegesEvaluator evaluator; @@ -78,9 +77,15 @@ public class TenantInfoAction extends BaseRestHandler { private final AdminDNs adminDns; private final ConfigurationRepository configurationRepository; - public TenantInfoAction(final Settings settings, final RestController controller, - final PrivilegesEvaluator evaluator, final ThreadPool threadPool, final ClusterService clusterService, final AdminDNs adminDns, - final ConfigurationRepository configurationRepository) { + public TenantInfoAction( + final Settings settings, + final RestController controller, + final PrivilegesEvaluator evaluator, + final ThreadPool threadPool, + final ClusterService clusterService, + final AdminDNs adminDns, + final ConfigurationRepository configurationRepository + ) { super(); this.threadContext = threadPool.getThreadContext(); this.evaluator = evaluator; @@ -100,41 +105,41 @@ protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient cli @Override public void accept(RestChannel channel) throws Exception { - XContentBuilder builder = channel.newBuilder(); //NOSONAR + XContentBuilder builder = channel.newBuilder(); // NOSONAR BytesRestResponse response = null; try { - final User user = (User)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final User user = (User) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); - //only allowed for admins or the kibanaserveruser - if(!isAuthorized()) { - response = new BytesRestResponse(RestStatus.FORBIDDEN,""); + // only allowed for admins or the kibanaserveruser + if (!isAuthorized()) { + response = new BytesRestResponse(RestStatus.FORBIDDEN, ""); } else { - builder.startObject(); + builder.startObject(); - final SortedMap lookup = clusterService.state().metadata().getIndicesLookup(); - for(final String indexOrAlias: lookup.keySet()) { - final String tenant = tenantNameForIndex(indexOrAlias); - if(tenant != null) { - builder.field(indexOrAlias, tenant); - } - } + final SortedMap lookup = clusterService.state().metadata().getIndicesLookup(); + for (final String indexOrAlias : lookup.keySet()) { + final String tenant = tenantNameForIndex(indexOrAlias); + if (tenant != null) { + builder.field(indexOrAlias, tenant); + } + } - builder.endObject(); + builder.endObject(); - response = new BytesRestResponse(RestStatus.OK, builder); + response = new BytesRestResponse(RestStatus.OK, builder); } } catch (final Exception e1) { log.error(e1.toString()); - builder = channel.newBuilder(); //NOSONAR + 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) { + if (builder != null) { builder.close(); } } @@ -145,7 +150,7 @@ public void accept(RestChannel channel) throws Exception { } private boolean isAuthorized() { - final User user = (User)threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); + final User user = (User) threadContext.getTransient(ConfigConstants.OPENDISTRO_SECURITY_USER); if (user == null) { return false; @@ -173,38 +178,38 @@ private boolean isAuthorized() { } private final SecurityDynamicConfiguration load(final CType config, boolean logComplianceEvent) { - SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex(Collections.singleton(config), logComplianceEvent).get(config).deepClone(); + SecurityDynamicConfiguration loaded = configurationRepository.getConfigurationsFromIndex( + Collections.singleton(config), + logComplianceEvent + ).get(config).deepClone(); return DynamicConfigFactory.addStatics(loaded); } private String tenantNameForIndex(String index) { - String[] indexParts; - if(index == null - || (indexParts = index.split("_")).length != 3 - ) { - return null; - } - - - if(!indexParts[0].equals(evaluator.dashboardsIndex())) { - 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 {} looks like a Security tenant index but we cannot parse the hashcode so we ignore it.", index); - return null; - } + String[] indexParts; + if (index == null || (indexParts = index.split("_")).length != 3) { + return null; + } + + if (!indexParts[0].equals(evaluator.dashboardsIndex())) { + 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 {} looks like a Security tenant index but we cannot parse the hashcode so we ignore it.", index); + return null; + } } @Override @@ -212,5 +217,4 @@ public String getName() { return "Tenant Info Action"; } - } diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModel.java b/src/main/java/org/opensearch/security/securityconf/ConfigModel.java index b4b2a9dd20..653ff23896 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModel.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModel.java @@ -32,11 +32,12 @@ import org.opensearch.common.transport.TransportAddress; import org.opensearch.security.user.User; - public abstract class ConfigModel { public abstract Map mapTenants(User user, Set roles); + public abstract Set mapSecurityRoles(User user, TransportAddress caller); + public abstract SecurityRoles getSecurityRoles(); public abstract Set getAllConfiguredTenantNames(); diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java index 7a978034f1..837dc0cff0 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV6.java @@ -66,7 +66,6 @@ import static org.opensearch.cluster.metadata.IndexAbstraction.Type.ALIAS; - public class ConfigModelV6 extends ConfigModel { protected final Logger log = LogManager.getLogger(this.getClass()); @@ -78,18 +77,22 @@ public class ConfigModelV6 extends ConfigModel { private SecurityDynamicConfiguration roles; public ConfigModelV6( - SecurityDynamicConfiguration roles, - SecurityDynamicConfiguration actiongroups, - SecurityDynamicConfiguration rolesmapping, - DynamicConfigModel dcm, - Settings opensearchSettings) { + SecurityDynamicConfiguration roles, + SecurityDynamicConfiguration actiongroups, + SecurityDynamicConfiguration rolesmapping, + DynamicConfigModel dcm, + Settings opensearchSettings + ) { this.roles = roles; try { rolesMappingResolution = ConfigConstants.RolesMappingResolution.valueOf( - opensearchSettings.get(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, ConfigConstants.RolesMappingResolution.MAPPING_ONLY.toString()) - .toUpperCase()); + opensearchSettings.get( + ConfigConstants.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; @@ -137,15 +140,14 @@ private Set getGroupMembers(final String groupname) { private Set resolve(final SecurityDynamicConfiguration actionGroups, final String entry) { - // SG5 format, plain array - //List en = actionGroups.getAsList(DotPath.of(entry)); - //if (en.isEmpty()) { - // try SG6 format including readonly and permissions key - // en = actionGroups.getAsList(DotPath.of(entry + "." + ConfigConstants.CONFIGKEY_ACTION_GROUPS_PERMISSIONS)); - //} + // List en = actionGroups.getAsList(DotPath.of(entry)); + // if (en.isEmpty()) { + // try SG6 format including readonly and permissions key + // en = actionGroups.getAsList(DotPath.of(entry + "." + ConfigConstants.CONFIGKEY_ACTION_GROUPS_PERMISSIONS)); + // } - if(!actionGroups.getCEntries().containsKey(entry)) { + if (!actionGroups.getCEntries().containsKey(entry)) { return Collections.emptySet(); } @@ -153,27 +155,26 @@ private Set resolve(final SecurityDynamicConfiguration actionGroups, final Object actionGroupAsObject = actionGroups.getCEntries().get(entry); - if(actionGroupAsObject != null && actionGroupAsObject instanceof List) { + if (actionGroupAsObject != null && actionGroupAsObject instanceof List) { - for (final String perm: ((List) actionGroupAsObject)) { + for (final String perm : ((List) actionGroupAsObject)) { if (actionGroups.getCEntries().keySet().contains(perm)) { - ret.addAll(resolve(actionGroups,perm)); + ret.addAll(resolve(actionGroups, perm)); } else { ret.add(perm); } } - - } else if(actionGroupAsObject != null && actionGroupAsObject instanceof ActionGroupsV6) { - for (final String perm: ((ActionGroupsV6) actionGroupAsObject).getPermissions()) { + } else if (actionGroupAsObject != null && actionGroupAsObject instanceof ActionGroupsV6) { + for (final String perm : ((ActionGroupsV6) actionGroupAsObject).getPermissions()) { if (actionGroups.getCEntries().keySet().contains(perm)) { - ret.addAll(resolve(actionGroups,perm)); + ret.addAll(resolve(actionGroups, perm)); } else { ret.add(perm); } } } else { - throw new RuntimeException("Unable to handle "+actionGroupAsObject); + throw new RuntimeException("Unable to handle " + actionGroupAsObject); } return Collections.unmodifiableSet(ret); @@ -182,7 +183,7 @@ private Set resolve(final SecurityDynamicConfiguration actionGroups, @Override public Set resolvedActions(final List actions) { final Set resolvedActions = new HashSet(); - for (String string: actions) { + for (String string : actions) { final Set groups = getGroupMembers(string); if (groups.isEmpty()) { resolvedActions.add(string); @@ -201,7 +202,7 @@ private SecurityRoles reload(SecurityDynamicConfiguration settings) { final Set> futures = new HashSet<>(5000); final ExecutorService execs = Executors.newFixedThreadPool(10); - for(Entry securityRole: settings.getCEntries().entrySet()) { + for (Entry securityRole : settings.getCEntries().entrySet()) { Future future = execs.submit(new Callable() { @@ -209,61 +210,60 @@ private SecurityRoles reload(SecurityDynamicConfiguration settings) { public SecurityRole call() throws Exception { SecurityRole _securityRole = new SecurityRole(securityRole.getKey()); - if(securityRole.getValue() == null) { + if (securityRole.getValue() == null) { return null; } final Set permittedClusterActions = agr.resolvedActions(securityRole.getValue().getCluster()); _securityRole.addClusterPerms(permittedClusterActions); - //if(tenants != null) { - for(Entry tenant: securityRole.getValue().getTenants().entrySet()) { + // if(tenants != null) { + for (Entry tenant : securityRole.getValue().getTenants().entrySet()) { - //if(tenant.equals(user.getName())) { - // continue; - //} + // if(tenant.equals(user.getName())) { + // continue; + // } - if("RW".equalsIgnoreCase(tenant.getValue())) { - _securityRole.addTenant(new Tenant(tenant.getKey(), true)); - } else { - _securityRole.addTenant(new Tenant(tenant.getKey(), false)); - //if(_securityRole.tenants.stream().filter(t->t.tenant.equals(tenant)).count() > 0) { //RW outperforms RO - // _securityRole.addTenant(new Tenant(tenant, false)); - //} - } + if ("RW".equalsIgnoreCase(tenant.getValue())) { + _securityRole.addTenant(new Tenant(tenant.getKey(), true)); + } else { + _securityRole.addTenant(new Tenant(tenant.getKey(), 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(DotPath.of("indices")); - - for (final Entry permittedAliasesIndex : securityRole.getValue().getIndices().entrySet()) { + } + // } - //final String resolvedRole = securityRole; - //final String indexPattern = permittedAliasesIndex; + // final Map permittedAliasesIndices = + // securityRoleSettings.getGroups(DotPath.of("indices")); - final String dls = permittedAliasesIndex.getValue().get_dls_(); - final List fls = permittedAliasesIndex.getValue().get_fls_(); - final List maskedFields = permittedAliasesIndex.getValue().get_masked_fields_(); + for (final Entry permittedAliasesIndex : securityRole.getValue().getIndices().entrySet()) { - IndexPattern _indexPattern = new IndexPattern(permittedAliasesIndex.getKey()); - _indexPattern.setDlsQuery(dls); - _indexPattern.addFlsFields(fls); - _indexPattern.addMaskedFields(maskedFields); + // final String resolvedRole = securityRole; + // final String indexPattern = permittedAliasesIndex; - for(Entry> type: permittedAliasesIndex.getValue().getTypes().entrySet()) { - TypePerm typePerm = new TypePerm(type.getKey()); - final List perms = type.getValue(); - typePerm.addPerms(agr.resolvedActions(perms)); - _indexPattern.addTypePerms(typePerm); - } + final String dls = permittedAliasesIndex.getValue().get_dls_(); + final List fls = permittedAliasesIndex.getValue().get_fls_(); + final List maskedFields = permittedAliasesIndex.getValue().get_masked_fields_(); - _securityRole.addIndexPattern(_indexPattern); + IndexPattern _indexPattern = new IndexPattern(permittedAliasesIndex.getKey()); + _indexPattern.setDlsQuery(dls); + _indexPattern.addFlsFields(fls); + _indexPattern.addMaskedFields(maskedFields); + for (Entry> type : permittedAliasesIndex.getValue().getTypes().entrySet()) { + TypePerm typePerm = new TypePerm(type.getKey()); + final List perms = type.getValue(); + typePerm.addPerms(agr.resolvedActions(perms)); + _indexPattern.addTypePerms(typePerm); } + _securityRole.addIndexPattern(_indexPattern); - return _securityRole; + } + + return _securityRole; } }); @@ -296,8 +296,7 @@ public SecurityRole call() throws Exception { } } - - //beans + // beans public static class SecurityRoles implements org.opensearch.security.securityconf.SecurityRoles { @@ -326,18 +325,13 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; + 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; + if (other.roles != null) return false; + } else if (!roles.equals(other.roles)) return false; return true; } @@ -349,6 +343,7 @@ public String toString() { public Set getRoles() { return Collections.unmodifiableSet(roles); } + public Set getRoleNames() { return getRoles().stream().map(r -> r.getName()).collect(Collectors.toSet()); } @@ -363,10 +358,14 @@ public SecurityRoles filter(Set keep) { return retVal; } - @Override - public EvaluatedDlsFlsConfig getDlsFls(User user, boolean dfmEmptyOverwritesAll, IndexNameExpressionResolver resolver, ClusterService cs, - NamedXContentRegistry namedXContentRegistry) { + public EvaluatedDlsFlsConfig getDlsFls( + User user, + boolean dfmEmptyOverwritesAll, + IndexNameExpressionResolver resolver, + ClusterService cs, + NamedXContentRegistry namedXContentRegistry + ) { final Map> dlsQueries = new HashMap>(); final Map> flsFields = new HashMap>(); @@ -380,8 +379,9 @@ public EvaluatedDlsFlsConfig getDlsFls(User user, boolean dfmEmptyOverwritesAll, final Set maskedFields = ip.getMaskedFields(); Set concreteIndices = new HashSet<>(); - - if ((dls != null && dls.length() > 0) || (fls != null && fls.size() > 0) || (maskedFields != null && maskedFields.size() > 0)) { + if ((dls != null && dls.length() > 0) + || (fls != null && fls.size() > 0) + || (maskedFields != null && maskedFields.size() > 0)) { concreteIndices = ip.getResolvedIndexPattern(user, resolver, cs); } @@ -448,10 +448,14 @@ public EvaluatedDlsFlsConfig getDlsFls(User user, boolean dfmEmptyOverwritesAll, return new EvaluatedDlsFlsConfig(dlsQueries, flsFields, maskedFieldsMap); } - - - //opensearchDashboards special only, terms eval - public Set getAllPermittedIndicesForDashboards(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { + // opensearchDashboards special only, terms eval + public Set getAllPermittedIndicesForDashboards( + Resolved resolved, + User user, + String[] actions, + IndexNameExpressionResolver resolver, + ClusterService cs + ) { Set retVal = new HashSet<>(); for (SecurityRole sr : roles) { retVal.addAll(sr.getAllResolvedPermittedIndices(Resolved._LOCAL_ALL, user, actions, resolver, cs)); @@ -460,7 +464,7 @@ public Set getAllPermittedIndicesForDashboards(Resolved resolved, User u return Collections.unmodifiableSet(retVal); } - //dnfof only + // dnfof only public Set reduce(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { Set retVal = new HashSet<>(); for (SecurityRole sr : roles) { @@ -472,7 +476,7 @@ public Set reduce(Resolved resolved, User user, String[] actions, IndexN return Collections.unmodifiableSet(retVal); } - //return true on success + // return true on success public boolean get(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { for (SecurityRole sr : roles) { if (ConfigModelV6.impliesTypePerm(sr.getIpatterns(), resolved, user, actions, resolver, cs)) { @@ -489,16 +493,20 @@ public boolean impliesClusterPermissionPermission(String action) { @Override public boolean hasExplicitClusterPermissionPermission(String action) { - return roles.stream() - .map(r -> { - final WildcardMatcher m = WildcardMatcher.from(r.clusterPerms); - return m == WildcardMatcher.ANY ? WildcardMatcher.NONE : m; - }).filter(m -> m.test(action)).count() > 0; - } - - //rolespan - public boolean impliesTypePermGlobal(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, - ClusterService cs) { + return roles.stream().map(r -> { + final WildcardMatcher m = WildcardMatcher.from(r.clusterPerms); + return m == WildcardMatcher.ANY ? WildcardMatcher.NONE : m; + }).filter(m -> m.test(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 ConfigModelV6.impliesTypePerm(ipatterns, resolved, user, actions, resolver, cs); @@ -521,14 +529,19 @@ private boolean impliesClusterPermission(String action) { return WildcardMatcher.from(clusterPerms).test(action); } - //get indices which are permitted for the given types and actions - //dnfof + opensearchDashboards special only - private Set getAllResolvedPermittedIndices(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, - ClusterService cs) { + // get indices which are permitted for the given types and actions + // dnfof + opensearchDashboards 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) + // what if we cannot resolve one (for create purposes) boolean patternMatch = false; final Set tperms = p.getTypePerms(); for (TypePerm tp : tperms) { @@ -537,24 +550,25 @@ private Set getAllResolvedPermittedIndices(Resolved resolved, User user, } } if (patternMatch) { - //resolved but can contain patterns for nonexistent indices - final WildcardMatcher permitted = WildcardMatcher.from(p.getResolvedIndexPattern(user, resolver, cs)); //maybe they do not exist + // resolved but can contain patterns for nonexistent indices + final WildcardMatcher permitted = WildcardMatcher.from(p.getResolvedIndexPattern(user, resolver, cs)); // maybe they do + // not exist final Set res = new HashSet<>(); if (!resolved.isLocalAll() && !resolved.getAllIndices().contains("*") && !resolved.getAllIndices().contains("_all")) { - //resolved but can contain patterns for nonexistent indices + // resolved but can contain patterns for nonexistent indices resolved.getAllIndices().stream().filter(permitted).forEach(res::add); } else { - //we want all indices so just return what's permitted + // we want all indices so just return what's permitted - //#557 - //final String[] allIndices = resolver.concreteIndexNames(cs.state(), IndicesOptions.lenientExpandOpen(), "*"); + // #557 + // final String[] allIndices = resolver.concreteIndexNames(cs.state(), IndicesOptions.lenientExpandOpen(), "*"); Arrays.stream(cs.state().metadata().getConcreteAllOpenIndices()).filter(permitted).forEach(res::add); } retVal.addAll(res); } } - //all that we want and all thats permitted of them + // all that we want and all thats permitted of them return Collections.unmodifiableSet(retVal); } @@ -592,44 +606,43 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; + 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 (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 (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 (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; + 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; + 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 + // TODO filter out user tenants return Collections.unmodifiableSet(tenants); } @@ -647,7 +660,7 @@ public String getName() { } - //sg roles + // sg roles public static class IndexPattern { private final String indexPattern; private String dlsQuery; @@ -702,45 +715,42 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; + 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 (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 (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 (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 (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; + 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; + return System.lineSeparator() + + " indexPattern=" + + indexPattern + + System.lineSeparator() + + " dlsQuery=" + + dlsQuery + + System.lineSeparator() + + " fls=" + + fls + + System.lineSeparator() + + " typePerms=" + + typePerms; } public String getUnresolvedIndexPattern(User user) { @@ -752,11 +762,15 @@ private Set getResolvedIndexPattern(User user, IndexNameExpressionResolv WildcardMatcher matcher = WildcardMatcher.from(unresolved); String[] resolved = null; if (!(matcher instanceof WildcardMatcher.Exact)) { - final String[] aliasesForPermittedPattern = cs.state().getMetadata().getIndicesLookup().entrySet().stream() - .filter(e -> e.getValue().getType() == ALIAS) - .filter(e -> matcher.test(e.getKey())) - .map(e -> e.getKey()) - .toArray(String[]::new); + final String[] aliasesForPermittedPattern = cs.state() + .getMetadata() + .getIndicesLookup() + .entrySet() + .stream() + .filter(e -> e.getValue().getType() == ALIAS) + .filter(e -> matcher.test(e.getKey())) + .map(e -> e.getKey()) + .toArray(String[]::new); if (aliasesForPermittedPattern.length > 0) { resolved = resolver.concreteIndexNames(cs.state(), IndicesOptions.lenientExpandOpen(), aliasesForPermittedPattern); @@ -769,10 +783,7 @@ private Set getResolvedIndexPattern(User user, IndexNameExpressionResolv if (resolved == null || resolved.length == 0) { return ImmutableSet.of(unresolved); } else { - return ImmutableSet.builder() - .addAll(Arrays.asList(resolved)) - .add(unresolved) - .build(); + return ImmutableSet.builder().addAll(Arrays.asList(resolved)).add(unresolved).build(); } } @@ -820,29 +831,27 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; + 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 (other.perms != null) return false; + } else if (!perms.equals(other.perms)) return false; if (typeMatcher == null) { - if (other.typeMatcher != null) - return false; - } else if (!typeMatcher.equals(other.typeMatcher)) - return false; + if (other.typeMatcher != null) return false; + } else if (!typeMatcher.equals(other.typeMatcher)) return false; return true; } @Override public String toString() { - return System.lineSeparator() + " typePattern=" + typeMatcher + System.lineSeparator() + " perms=" + perms; + return System.lineSeparator() + + " typePattern=" + + typeMatcher + + System.lineSeparator() + + " perms=" + + perms; } public WildcardMatcher getTypeMatcher() { @@ -884,26 +893,25 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; + 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 (readWrite != other.readWrite) return false; if (tenant == null) { - if (other.tenant != null) - return false; - } else if (!tenant.equals(other.tenant)) - return false; + 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; + return System.lineSeparator() + + " tenant=" + + tenant + + System.lineSeparator() + + " readWrite=" + + readWrite; } } @@ -981,38 +989,43 @@ public boolean matches(String index, String type, String action) { } } - private static boolean impliesTypePerm(Set ipatterns, Resolved resolved, User user, String[] requestedActions, - IndexNameExpressionResolver resolver, ClusterService cs) { + private static boolean impliesTypePerm( + Set ipatterns, + Resolved resolved, + User user, + String[] requestedActions, + IndexNameExpressionResolver resolver, + ClusterService cs + ) { IndexMatcherAndTypePermissions[] indexMatcherAndTypePermissions; if (resolved.isLocalAll()) { // Only let localAll pass if there is an explicit privilege for a * index pattern - indexMatcherAndTypePermissions = ipatterns - .stream() - .filter(indexPattern -> "*".equals(indexPattern.getUnresolvedIndexPattern(user))) - .map(p -> new IndexMatcherAndTypePermissions(p.getResolvedIndexPattern(user, resolver, cs), p.getTypePerms())) - .toArray(IndexMatcherAndTypePermissions[]::new); + indexMatcherAndTypePermissions = ipatterns.stream() + .filter(indexPattern -> "*".equals(indexPattern.getUnresolvedIndexPattern(user))) + .map(p -> new IndexMatcherAndTypePermissions(p.getResolvedIndexPattern(user, resolver, cs), p.getTypePerms())) + .toArray(IndexMatcherAndTypePermissions[]::new); } else { - indexMatcherAndTypePermissions = ipatterns - .stream() - .map(p -> new IndexMatcherAndTypePermissions(p.getResolvedIndexPattern(user, resolver, cs), p.getTypePerms())) - .toArray(IndexMatcherAndTypePermissions[]::new); + indexMatcherAndTypePermissions = ipatterns.stream() + .map(p -> new IndexMatcherAndTypePermissions(p.getResolvedIndexPattern(user, resolver, cs), p.getTypePerms())) + .toArray(IndexMatcherAndTypePermissions[]::new); } return resolved.getAllIndices() - .stream().allMatch(index -> - resolved.getTypes().stream().allMatch(type -> - Arrays.stream(requestedActions).allMatch(action -> - Arrays.stream(indexMatcherAndTypePermissions).anyMatch(ipatp -> - ipatp.matches(index, type, action) - ) - ) - ) - ); + .stream() + .allMatch( + index -> resolved.getTypes() + .stream() + .allMatch( + type -> Arrays.stream(requestedActions) + .allMatch( + action -> Arrays.stream(indexMatcherAndTypePermissions) + .anyMatch(ipatp -> ipatp.matches(index, type, action)) + ) + ) + ); } - - - //####### + // ####### private class TenantHolder { @@ -1023,37 +1036,39 @@ public TenantHolder(SecurityDynamicConfiguration roles) { final ExecutorService execs = Executors.newFixedThreadPool(10); - for(Entry securityRole: roles.getCEntries().entrySet()) { + for (Entry securityRole : roles.getCEntries().entrySet()) { - if(securityRole.getValue() == null) { + if (securityRole.getValue() == null) { continue; } - Future>>> future = execs.submit(new Callable>>>() { - @Override - public Tuple>> call() throws Exception { - final Set> tuples = new HashSet<>(); - final Map tenants = securityRole.getValue().getTenants(); - - if (tenants != null) { - - for (String tenant : tenants.keySet()) { - - if ("RW".equalsIgnoreCase(tenants.get(tenant))) { - //RW - tuples.add(new Tuple(tenant, true)); - } else { - //RO - //if(!tenantsMM.containsValue(value)) { //RW outperforms RO - tuples.add(new Tuple(tenant, false)); - //} + Future>>> future = execs.submit( + new Callable>>>() { + @Override + public Tuple>> call() throws Exception { + final Set> tuples = new HashSet<>(); + final Map tenants = securityRole.getValue().getTenants(); + + if (tenants != null) { + + for (String tenant : tenants.keySet()) { + + if ("RW".equalsIgnoreCase(tenants.get(tenant))) { + // RW + tuples.add(new Tuple(tenant, true)); + } else { + // RO + // if(!tenantsMM.containsValue(value)) { //RW outperforms RO + tuples.add(new Tuple(tenant, false)); + // } + } } } - } - return new Tuple>>(securityRole.getKey(), tuples); + return new Tuple>>(securityRole.getKey(), tuples); + } } - }); + ); futures.add(future); @@ -1069,7 +1084,9 @@ public Tuple>> call() throws Exception { } try { - final SetMultimap> tenantsMM_ = SetMultimapBuilder.hashKeys(futures.size()).hashSetValues(16).build(); + final SetMultimap> tenantsMM_ = SetMultimapBuilder.hashKeys(futures.size()) + .hashSetValues(16) + .build(); for (Future>>> future : futures) { Tuple>> result = future.get(); @@ -1097,14 +1114,18 @@ public Map mapTenants(final User user, Set roles) { final Map result = new HashMap<>(roles.size()); result.put(user.getName(), true); - tenantsMM.entries().stream().filter(e -> roles.contains(e.getKey())).filter(e -> !user.getName().equals(e.getValue().v1())).forEach(e -> { - final String tenant = e.getValue().v1(); - final boolean rw = e.getValue().v2(); + tenantsMM.entries() + .stream() + .filter(e -> roles.contains(e.getKey())) + .filter(e -> !user.getName().equals(e.getValue().v1())) + .forEach(e -> { + final String tenant = e.getValue().v1(); + final boolean rw = e.getValue().v2(); - if (rw || !result.containsKey(tenant)) { //RW outperforms RO - result.put(tenant, rw); - } - }); + if (rw || !result.containsKey(tenant)) { // RW outperforms RO + result.put(tenant, rw); + } + }); return Collections.unmodifiableMap(result); } @@ -1171,7 +1192,7 @@ private Set map(final User user, final TransportAddress caller) { final Set securityRoles = new HashSet<>(); if (rolesMappingResolution == ConfigConstants.RolesMappingResolution.BOTH - || rolesMappingResolution == ConfigConstants.RolesMappingResolution.BACKENDROLES_ONLY) { + || rolesMappingResolution == ConfigConstants.RolesMappingResolution.BACKENDROLES_ONLY) { if (log.isDebugEnabled()) { log.debug("Pass backendroles from {}", user); } @@ -1179,7 +1200,7 @@ private Set map(final User user, final TransportAddress caller) { } if (((rolesMappingResolution == ConfigConstants.RolesMappingResolution.BOTH - || rolesMappingResolution == ConfigConstants.RolesMappingResolution.MAPPING_ONLY))) { + || rolesMappingResolution == ConfigConstants.RolesMappingResolution.MAPPING_ONLY))) { for (String p : WildcardMatcher.getAllMatchingPatterns(userMatchers, user.getName())) { securityRoles.addAll(users.get(p)); @@ -1196,7 +1217,7 @@ private Set map(final User user, final TransportAddress caller) { } if (caller != null) { - //IPV4 or IPv6 (compressed and without scope identifiers) + // IPV4 or IPv6 (compressed and without scope identifiers) final String ipAddress = caller.getAddress(); final List hostMatchers = WildcardMatcher.matchers(hosts.keySet()); @@ -1205,7 +1226,7 @@ private Set map(final User user, final TransportAddress caller) { } if (caller.address() != null - && (hostResolverMode.equalsIgnoreCase("ip-hostname") || hostResolverMode.equalsIgnoreCase("ip-hostname-lookup"))) { + && (hostResolverMode.equalsIgnoreCase("ip-hostname") || hostResolverMode.equalsIgnoreCase("ip-hostname-lookup"))) { final String hostName = caller.address().getHostString(); for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, hostName)) { @@ -1229,10 +1250,6 @@ private Set map(final User user, final TransportAddress caller) { } } - - - - public Map mapTenants(User user, Set roles) { return tenantHolder.mapTenants(user, roles); } diff --git a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java index 560cfb8a6d..1fb6e4da0e 100644 --- a/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/ConfigModelV7.java @@ -83,20 +83,24 @@ public class ConfigModelV7 extends ConfigModel { private SecurityDynamicConfiguration tenants; public ConfigModelV7( - SecurityDynamicConfiguration roles, - SecurityDynamicConfiguration rolemappings, - SecurityDynamicConfiguration actiongroups, - SecurityDynamicConfiguration tenants, - DynamicConfigModel dcm, - Settings opensearchSettings) { + SecurityDynamicConfiguration roles, + SecurityDynamicConfiguration rolemappings, + SecurityDynamicConfiguration actiongroups, + SecurityDynamicConfiguration tenants, + DynamicConfigModel dcm, + Settings opensearchSettings + ) { this.roles = roles; this.tenants = tenants; try { rolesMappingResolution = ConfigConstants.RolesMappingResolution.valueOf( - opensearchSettings.get(ConfigConstants.SECURITY_ROLES_MAPPING_RESOLUTION, ConfigConstants.RolesMappingResolution.MAPPING_ONLY.toString()) - .toUpperCase()); + opensearchSettings.get( + ConfigConstants.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; @@ -134,15 +138,14 @@ private Set getGroupMembers(final String groupname) { private Set resolve(final SecurityDynamicConfiguration actionGroups, final String entry) { - // SG5 format, plain array - //List en = actionGroups.getAsList(DotPath.of(entry)); - //if (en.isEmpty()) { - // try SG6 format including readonly and permissions key - // en = actionGroups.getAsList(DotPath.of(entry + "." + ConfigConstants.CONFIGKEY_ACTION_GROUPS_PERMISSIONS)); - //} + // List en = actionGroups.getAsList(DotPath.of(entry)); + // if (en.isEmpty()) { + // try SG6 format including readonly and permissions key + // en = actionGroups.getAsList(DotPath.of(entry + "." + ConfigConstants.CONFIGKEY_ACTION_GROUPS_PERMISSIONS)); + // } - if(!actionGroups.getCEntries().containsKey(entry)) { + if (!actionGroups.getCEntries().containsKey(entry)) { return Collections.emptySet(); } @@ -150,27 +153,26 @@ private Set resolve(final SecurityDynamicConfiguration actionGroups, final Object actionGroupAsObject = actionGroups.getCEntries().get(entry); - if(actionGroupAsObject != null && actionGroupAsObject instanceof List) { + if (actionGroupAsObject != null && actionGroupAsObject instanceof List) { - for (final String perm: ((List) actionGroupAsObject)) { + for (final String perm : ((List) actionGroupAsObject)) { if (actionGroups.getCEntries().keySet().contains(perm)) { - ret.addAll(resolve(actionGroups,perm)); + ret.addAll(resolve(actionGroups, perm)); } else { ret.add(perm); } } - - } else if(actionGroupAsObject != null && actionGroupAsObject instanceof ActionGroupsV7) { - for (final String perm: ((ActionGroupsV7) actionGroupAsObject).getAllowed_actions()) { + } else if (actionGroupAsObject != null && actionGroupAsObject instanceof ActionGroupsV7) { + for (final String perm : ((ActionGroupsV7) actionGroupAsObject).getAllowed_actions()) { if (actionGroups.getCEntries().keySet().contains(perm)) { - ret.addAll(resolve(actionGroups,perm)); + ret.addAll(resolve(actionGroups, perm)); } else { ret.add(perm); } } } else { - throw new RuntimeException("Unable to handle "+actionGroupAsObject); + throw new RuntimeException("Unable to handle " + actionGroupAsObject); } return Collections.unmodifiableSet(ret); @@ -179,7 +181,7 @@ private Set resolve(final SecurityDynamicConfiguration actionGroups, @Override public Set resolvedActions(final List actions) { final Set resolvedActions = new HashSet(); - for (String string: actions) { + for (String string : actions) { final Set groups = getGroupMembers(string); if (groups.isEmpty()) { resolvedActions.add(string); @@ -198,7 +200,7 @@ private SecurityRoles reload(SecurityDynamicConfiguration settings) { final Set> futures = new HashSet<>(5000); final ExecutorService execs = Executors.newFixedThreadPool(10); - for(Entry securityRole: settings.getCEntries().entrySet()) { + for (Entry securityRole : settings.getCEntries().entrySet()) { Future future = execs.submit(new Callable() { @@ -206,54 +208,53 @@ private SecurityRoles reload(SecurityDynamicConfiguration settings) { public SecurityRole call() throws Exception { SecurityRole.Builder _securityRole = new SecurityRole.Builder(securityRole.getKey()); - if(securityRole.getValue() == null) { + if (securityRole.getValue() == null) { return null; } final Set permittedClusterActions = agr.resolvedActions(securityRole.getValue().getCluster_permissions()); _securityRole.addClusterPerms(permittedClusterActions); - /*for(RoleV7.Tenant tenant: securityRole.getValue().getTenant_permissions()) { + /*for(RoleV7.Tenant tenant: securityRole.getValue().getTenant_permissions()) { - //if(tenant.equals(user.getName())) { - // continue; - //} + //if(tenant.equals(user.getName())) { + // continue; + //} - if(isTenantsRw(tenant)) { - _securityRole.addTenant(new Tenant(tenant.getKey(), true)); - } else { - _securityRole.addTenant(new Tenant(tenant.getKey(), false)); - } - }*/ + if(isTenantsRw(tenant)) { + _securityRole.addTenant(new Tenant(tenant.getKey(), true)); + } else { + _securityRole.addTenant(new Tenant(tenant.getKey(), false)); + } + }*/ - for (final Index permittedAliasesIndex : securityRole.getValue().getIndex_permissions()) { + for (final Index permittedAliasesIndex : securityRole.getValue().getIndex_permissions()) { - final String dls = permittedAliasesIndex.getDls(); - final List fls = permittedAliasesIndex.getFls(); - final List maskedFields = permittedAliasesIndex.getMasked_fields(); + final String dls = permittedAliasesIndex.getDls(); + final List fls = permittedAliasesIndex.getFls(); + final List maskedFields = permittedAliasesIndex.getMasked_fields(); - for(String pat: permittedAliasesIndex.getIndex_patterns()) { - IndexPattern _indexPattern = new IndexPattern(pat); - _indexPattern.setDlsQuery(dls); - _indexPattern.addFlsFields(fls); - _indexPattern.addMaskedFields(maskedFields); - _indexPattern.addPerm(agr.resolvedActions(permittedAliasesIndex.getAllowed_actions())); + for (String pat : permittedAliasesIndex.getIndex_patterns()) { + IndexPattern _indexPattern = new IndexPattern(pat); + _indexPattern.setDlsQuery(dls); + _indexPattern.addFlsFields(fls); + _indexPattern.addMaskedFields(maskedFields); + _indexPattern.addPerm(agr.resolvedActions(permittedAliasesIndex.getAllowed_actions())); - /*for(Entry> type: permittedAliasesIndex.getValue().getTypes(-).entrySet()) { - TypePerm typePerm = new TypePerm(type.getKey()); - final List perms = type.getValue(); - typePerm.addPerms(agr.resolvedActions(perms)); - _indexPattern.addTypePerms(typePerm); - }*/ + /*for(Entry> type: permittedAliasesIndex.getValue().getTypes(-).entrySet()) { + TypePerm typePerm = new TypePerm(type.getKey()); + final List perms = type.getValue(); + typePerm.addPerms(agr.resolvedActions(perms)); + _indexPattern.addTypePerms(typePerm); + }*/ - _securityRole.addIndexPattern(_indexPattern); - - } + _securityRole.addIndexPattern(_indexPattern); } + } - return _securityRole.build(); + return _securityRole.build(); } }); @@ -286,8 +287,7 @@ public SecurityRole call() throws Exception { } } - - //beans + // beans public static class SecurityRoles implements org.opensearch.security.securityconf.SecurityRoles { @@ -316,18 +316,13 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; + 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; + if (other.roles != null) return false; + } else if (!roles.equals(other.roles)) return false; return true; } @@ -354,14 +349,17 @@ public SecurityRoles filter(Set keep) { return retVal; } - @Override - public EvaluatedDlsFlsConfig getDlsFls(User user, boolean dfmEmptyOverwritesAll, IndexNameExpressionResolver resolver, ClusterService cs, - NamedXContentRegistry namedXContentRegistry) { - + public EvaluatedDlsFlsConfig getDlsFls( + User user, + boolean dfmEmptyOverwritesAll, + IndexNameExpressionResolver resolver, + ClusterService cs, + NamedXContentRegistry namedXContentRegistry + ) { if (!containsDlsFlsConfig()) { - if(log.isDebugEnabled()) { + if (log.isDebugEnabled()) { log.debug("No fls or dls found for {} in {} security roles", user, roles.size()); } @@ -382,17 +380,17 @@ public EvaluatedDlsFlsConfig getDlsFls(User user, boolean dfmEmptyOverwritesAll, for (SecurityRole role : roles) { for (IndexPattern ip : role.getIpatterns()) { - final Set concreteIndices = ip.concreteIndexNames(user, resolver, cs); - String dls = ip.getDlsQuery(user); + final Set concreteIndices = ip.concreteIndexNames(user, resolver, cs); + String dls = ip.getDlsQuery(user); - if (dls != null && dls.length() > 0) { + if (dls != null && dls.length() > 0) { - for (String concreteIndex : concreteIndices) { - dlsQueriesByIndex.computeIfAbsent(concreteIndex, (key) -> new HashSet()).add(dls); - } - } else if (dfmEmptyOverwritesAll) { - noDlsConcreteIndices.addAll(concreteIndices); - } + for (String concreteIndex : concreteIndices) { + dlsQueriesByIndex.computeIfAbsent(concreteIndex, (key) -> new HashSet()).add(dls); + } + } else if (dfmEmptyOverwritesAll) { + noDlsConcreteIndices.addAll(concreteIndices); + } Set fls = ip.getFls(); @@ -429,12 +427,21 @@ public EvaluatedDlsFlsConfig getDlsFls(User user, boolean dfmEmptyOverwritesAll, } if (dfmEmptyOverwritesAll) { if (log.isDebugEnabled()) { - log.debug("Index patterns with no dls queries attached: {} - They will be removed from {}", noDlsConcreteIndices, - dlsQueriesByIndex.keySet()); - log.debug("Index patterns with no fls fields attached: {} - They will be removed from {}", noFlsConcreteIndices, - flsFields.keySet()); - log.debug("Index patterns with no masked fields attached: {} - They will be removed from {}", noMaskedFieldConcreteIndices, - maskedFieldsMap.keySet()); + log.debug( + "Index patterns with no dls queries attached: {} - They will be removed from {}", + noDlsConcreteIndices, + dlsQueriesByIndex.keySet() + ); + log.debug( + "Index patterns with no fls fields attached: {} - They will be removed from {}", + noFlsConcreteIndices, + flsFields.keySet() + ); + log.debug( + "Index patterns with no masked fields attached: {} - They will be removed from {}", + noMaskedFieldConcreteIndices, + maskedFieldsMap.keySet() + ); } // removing the indices that do not have D/M/F restrictions // from the keySet will also modify the underlying map @@ -446,9 +453,14 @@ public EvaluatedDlsFlsConfig getDlsFls(User user, boolean dfmEmptyOverwritesAll, return new EvaluatedDlsFlsConfig(dlsQueriesByIndex, flsFields, maskedFieldsMap); } - - //opensearchDashboards special only, terms eval - public Set getAllPermittedIndicesForDashboards(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { + // opensearchDashboards special only, terms eval + public Set getAllPermittedIndicesForDashboards( + Resolved resolved, + User user, + String[] actions, + IndexNameExpressionResolver resolver, + ClusterService cs + ) { Set retVal = new HashSet<>(); for (SecurityRole sr : roles) { retVal.addAll(sr.getAllResolvedPermittedIndices(Resolved._LOCAL_ALL, user, actions, resolver, cs)); @@ -457,7 +469,7 @@ public Set getAllPermittedIndicesForDashboards(Resolved resolved, User u return Collections.unmodifiableSet(retVal); } - //dnfof only + // dnfof only public Set reduce(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { Set retVal = new HashSet<>(); for (SecurityRole sr : roles) { @@ -469,7 +481,7 @@ public Set reduce(Resolved resolved, User user, String[] actions, IndexN return Collections.unmodifiableSet(retVal); } - //return true on success + // return true on success public boolean get(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs) { for (SecurityRole sr : roles) { if (ConfigModelV7.impliesTypePerm(sr.getIpatterns(), resolved, user, actions, resolver, cs)) { @@ -487,13 +499,19 @@ public boolean impliesClusterPermissionPermission(String action) { @Override public boolean hasExplicitClusterPermissionPermission(String action) { return roles.stream() - .map(r -> r.clusterPerms == WildcardMatcher.ANY ? WildcardMatcher.NONE : r.clusterPerms) - .filter(m -> m.test(action)).count() > 0; - } - - //rolespan - public boolean impliesTypePermGlobal(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, - ClusterService cs) { + .map(r -> r.clusterPerms == WildcardMatcher.ANY ? WildcardMatcher.NONE : r.clusterPerms) + .filter(m -> m.test(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 ConfigModelV7.impliesTypePerm(ipatterns, resolved, user, actions, resolver, cs); @@ -521,6 +539,7 @@ public static final class Builder { private final String name; private final Set clusterPerms = new HashSet<>(); private final Set ipatterns = new HashSet<>(); + public Builder(String name) { this.name = Objects.requireNonNull(name); } @@ -537,7 +556,6 @@ public Builder addClusterPerms(Collection clusterPerms) { return this; } - public SecurityRole build() { return new SecurityRole(name, ipatterns, WildcardMatcher.from(clusterPerms)); } @@ -553,34 +571,40 @@ private boolean impliesClusterPermission(String action) { return clusterPerms.test(action); } - //get indices which are permitted for the given types and actions - //dnfof + opensearchDashboards special only - private Set getAllResolvedPermittedIndices(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, - ClusterService cs) { + // get indices which are permitted for the given types and actions + // dnfof + opensearchDashboards 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) + // what if we cannot resolve one (for create purposes) final boolean patternMatch = p.getPerms().matchAll(actions); -// 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); -// } -// } + // 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 WildcardMatcher permitted = WildcardMatcher.from(p.attemptResolveIndexNames(user, resolver, cs)); //maybe they do not exist + // resolved but can contain patterns for nonexistent indices + final WildcardMatcher permitted = WildcardMatcher.from(p.attemptResolveIndexNames(user, resolver, cs)); // maybe they do + // not exist final Set res = new HashSet<>(); if (!resolved.isLocalAll() && !resolved.getAllIndices().contains("*") && !resolved.getAllIndices().contains("_all")) { - //resolved but can contain patterns for nonexistent indices + // resolved but can contain patterns for nonexistent indices resolved.getAllIndices().stream().filter(permitted).forEach(res::add); } else { - //we want all indices so just return what's permitted + // we want all indices so just return what's permitted - //#557 - //final String[] allIndices = resolver.concreteIndexNames(cs.state(), IndicesOptions.lenientExpandOpen(), "*"); + // #557 + // final String[] allIndices = resolver.concreteIndexNames(cs.state(), IndicesOptions.lenientExpandOpen(), "*"); final String[] allIndices = cs.state().metadata().getConcreteAllOpenIndices(); Arrays.stream(allIndices).filter(permitted).forEach(res::add); } @@ -588,7 +612,7 @@ private Set getAllResolvedPermittedIndices(Resolved resolved, User user, } } - //all that we want and all thats permitted of them + // all that we want and all thats permitted of them return Collections.unmodifiableSet(retVal); } @@ -606,52 +630,50 @@ public int hashCode() { 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()); + // 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; + 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 (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 (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; + 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() - + " ipatterns=" + ipatterns + System.lineSeparator() + " clusterPerms=" + clusterPerms; - } - - //public Set getTenants(User user) { - // //TODO filter out user tenants - // return Collections.unmodifiableSet(tenants); - //} + return System.lineSeparator() + + " " + + name + + 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); @@ -663,7 +685,7 @@ public String getName() { } - //sg roles + // sg roles public static class IndexPattern { private final String indexPattern; private String dlsQuery; @@ -718,45 +740,42 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; + 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 (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 (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 (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 (other.indexPattern != null) return false; + } else if (!indexPattern.equals(other.indexPattern)) return false; if (perms == null) { - if (other.perms != null) - return false; - } else if (!perms.equals(other.perms)) - return false; + if (other.perms != null) return false; + } else if (!perms.equals(other.perms)) return false; return true; } @Override public String toString() { - return System.lineSeparator() + " indexPattern=" + indexPattern + System.lineSeparator() + " dlsQuery=" + dlsQuery - + System.lineSeparator() + " fls=" + fls + System.lineSeparator() + " perms=" + perms; + return System.lineSeparator() + + " indexPattern=" + + indexPattern + + System.lineSeparator() + + " dlsQuery=" + + dlsQuery + + System.lineSeparator() + + " fls=" + + fls + + System.lineSeparator() + + " perms=" + + perms; } public String getUnresolvedIndexPattern(User user) { @@ -773,27 +792,45 @@ public Set attemptResolveIndexNames(final User user, final IndexNameExpr return getResolvedIndexPattern(user, resolver, cs, true); } - public Set getResolvedIndexPattern(final User user, final IndexNameExpressionResolver resolver, final ClusterService cs, final boolean appendUnresolved) { + public Set getResolvedIndexPattern( + final User user, + final IndexNameExpressionResolver resolver, + final ClusterService cs, + final boolean appendUnresolved + ) { final String unresolved = getUnresolvedIndexPattern(user); final ImmutableSet.Builder resolvedIndices = new ImmutableSet.Builder<>(); final WildcardMatcher matcher = WildcardMatcher.from(unresolved); boolean includeDataStreams = true; if (!(matcher instanceof WildcardMatcher.Exact)) { - final String[] aliasesAndDataStreamsForPermittedPattern = cs.state().getMetadata().getIndicesLookup().entrySet().stream() - .filter(e -> (e.getValue().getType() == ALIAS) || (e.getValue().getType() == DATA_STREAM)) - .filter(e -> matcher.test(e.getKey())) - .map(e -> e.getKey()) - .toArray(String[]::new); + final String[] aliasesAndDataStreamsForPermittedPattern = cs.state() + .getMetadata() + .getIndicesLookup() + .entrySet() + .stream() + .filter(e -> (e.getValue().getType() == ALIAS) || (e.getValue().getType() == DATA_STREAM)) + .filter(e -> matcher.test(e.getKey())) + .map(e -> e.getKey()) + .toArray(String[]::new); if (aliasesAndDataStreamsForPermittedPattern.length > 0) { - final String[] resolvedAliasesAndDataStreamIndices = resolver.concreteIndexNames(cs.state(), - IndicesOptions.lenientExpandOpen(), includeDataStreams, aliasesAndDataStreamsForPermittedPattern); + final String[] resolvedAliasesAndDataStreamIndices = resolver.concreteIndexNames( + cs.state(), + IndicesOptions.lenientExpandOpen(), + includeDataStreams, + aliasesAndDataStreamsForPermittedPattern + ); resolvedIndices.addAll(Arrays.asList(resolvedAliasesAndDataStreamIndices)); } } if (Strings.isNotBlank(unresolved)) { - final String[] resolvedIndicesFromPattern = resolver.concreteIndexNames(cs.state(), IndicesOptions.lenientExpandOpen(), includeDataStreams, unresolved); + final String[] resolvedIndicesFromPattern = resolver.concreteIndexNames( + cs.state(), + IndicesOptions.lenientExpandOpen(), + includeDataStreams, + unresolved + ); resolvedIndices.addAll(Arrays.asList(resolvedIndicesFromPattern)); } @@ -831,7 +868,6 @@ public WildcardMatcher getPerms() { return WildcardMatcher.from(perms); } - } /*public static class TypePerm { @@ -928,26 +964,25 @@ public int hashCode() { @Override public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; + 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 (readWrite != other.readWrite) return false; if (tenant == null) { - if (other.tenant != null) - return false; - } else if (!tenant.equals(other.tenant)) - return false; + 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; + return System.lineSeparator() + + " tenant=" + + tenant + + System.lineSeparator() + + " readWrite=" + + readWrite; } } @@ -997,6 +1032,7 @@ private static String toQuotedCommaSeparatedString(final Set roles) { private static final class IndexMatcherAndPermissions { private WildcardMatcher matcher; private WildcardMatcher perms; + public IndexMatcherAndPermissions(Set patterns, Set perms) { this.matcher = WildcardMatcher.from(patterns); this.perms = WildcardMatcher.from(perms); @@ -1007,31 +1043,31 @@ public boolean matches(String index, String action) { } } - private static boolean impliesTypePerm(Set ipatterns, Resolved resolved, User user, String[] requestedActions, - IndexNameExpressionResolver resolver, ClusterService cs) { + private static boolean impliesTypePerm( + Set ipatterns, + Resolved resolved, + User user, + String[] requestedActions, + IndexNameExpressionResolver resolver, + ClusterService cs + ) { Set resolvedRequestedIndices = resolved.getAllIndices(); IndexMatcherAndPermissions[] indexMatcherAndPermissions; if (resolved.isLocalAll()) { - indexMatcherAndPermissions = ipatterns - .stream() - .filter(indexPattern -> "*".equals(indexPattern.getUnresolvedIndexPattern(user))) - .map(p -> new IndexMatcherAndPermissions(p.attemptResolveIndexNames(user, resolver, cs), p.perms)) - .toArray(IndexMatcherAndPermissions[]::new); + indexMatcherAndPermissions = ipatterns.stream() + .filter(indexPattern -> "*".equals(indexPattern.getUnresolvedIndexPattern(user))) + .map(p -> new IndexMatcherAndPermissions(p.attemptResolveIndexNames(user, resolver, cs), p.perms)) + .toArray(IndexMatcherAndPermissions[]::new); } else { - indexMatcherAndPermissions = ipatterns - .stream() - .map(p -> new IndexMatcherAndPermissions(p.attemptResolveIndexNames(user, resolver, cs), p.perms)) - .toArray(IndexMatcherAndPermissions[]::new); - } - return resolvedRequestedIndices - .stream() - .allMatch(index -> - Arrays.stream(requestedActions).allMatch(action -> - Arrays.stream(indexMatcherAndPermissions).anyMatch(ipap -> - ipap.matches(index, action) - ) - ) - ); + indexMatcherAndPermissions = ipatterns.stream() + .map(p -> new IndexMatcherAndPermissions(p.attemptResolveIndexNames(user, resolver, cs), p.perms)) + .toArray(IndexMatcherAndPermissions[]::new); + } + return resolvedRequestedIndices.stream() + .allMatch( + index -> Arrays.stream(requestedActions) + .allMatch(action -> Arrays.stream(indexMatcherAndPermissions).anyMatch(ipap -> ipap.matches(index, action))) + ); } private class TenantHolder { @@ -1043,38 +1079,54 @@ public TenantHolder(SecurityDynamicConfiguration roles, SecurityDynamicC final ExecutorService execs = Executors.newFixedThreadPool(10); - for(Entry securityRole: roles.getCEntries().entrySet()) { + for (Entry securityRole : roles.getCEntries().entrySet()) { - if(securityRole.getValue() == null) { + if (securityRole.getValue() == null) { continue; } - Future>>> future = execs.submit(new Callable>>>() { - @Override - public Tuple>> call() throws Exception { - final Set> tuples = new HashSet<>(); - final List tenants = securityRole.getValue().getTenant_permissions(); - if (tenants != null) { - - for (RoleV7.Tenant tenant : tenants) { - - // find Wildcarded tenant patterns - List matchingTenants = WildcardMatcher.from(tenant.getTenant_patterns()).getMatchAny(definedTenants.getCEntries().keySet(), Collectors.toList()) ; - for(String matchingTenant: matchingTenants ) { - tuples.add(new Tuple(matchingTenant, agr.resolvedActions(tenant.getAllowed_actions()).contains("kibana:saved_objects/*/write"))); - } - // find parameter substitution specified tenant - Pattern parameterPattern = Pattern.compile("^\\$\\{attr"); - List matchingParameterTenantList = tenant.getTenant_patterns().stream().filter(parameterPattern.asPredicate()).collect(Collectors.toList()); - for(String matchingParameterTenant : matchingParameterTenantList ) { - tuples.add(new Tuple(matchingParameterTenant,agr.resolvedActions(tenant.getAllowed_actions()).contains("kibana:saved_objects/*/write"))) ; + Future>>> future = execs.submit( + new Callable>>>() { + @Override + public Tuple>> call() throws Exception { + final Set> tuples = new HashSet<>(); + final List tenants = securityRole.getValue().getTenant_permissions(); + if (tenants != null) { + + for (RoleV7.Tenant tenant : tenants) { + + // find Wildcarded tenant patterns + List matchingTenants = WildcardMatcher.from(tenant.getTenant_patterns()) + .getMatchAny(definedTenants.getCEntries().keySet(), Collectors.toList()); + for (String matchingTenant : matchingTenants) { + tuples.add( + new Tuple( + matchingTenant, + agr.resolvedActions(tenant.getAllowed_actions()).contains("kibana:saved_objects/*/write") + ) + ); + } + // find parameter substitution specified tenant + Pattern parameterPattern = Pattern.compile("^\\$\\{attr"); + List matchingParameterTenantList = tenant.getTenant_patterns() + .stream() + .filter(parameterPattern.asPredicate()) + .collect(Collectors.toList()); + for (String matchingParameterTenant : matchingParameterTenantList) { + tuples.add( + new Tuple( + matchingParameterTenant, + agr.resolvedActions(tenant.getAllowed_actions()).contains("kibana:saved_objects/*/write") + ) + ); + } } } - } - return new Tuple>>(securityRole.getKey(), tuples); + return new Tuple>>(securityRole.getKey(), tuples); + } } - }); + ); futures.add(future); @@ -1090,7 +1142,9 @@ public Tuple>> call() throws Exception { } try { - final SetMultimap> tenantsMM_ = SetMultimapBuilder.hashKeys(futures.size()).hashSetValues(16).build(); + final SetMultimap> tenantsMM_ = SetMultimapBuilder.hashKeys(futures.size()) + .hashSetValues(16) + .build(); for (Future>>> future : futures) { Tuple>> result = future.get(); @@ -1118,32 +1172,33 @@ public Map mapTenants(final User user, Set roles) { final Map result = new HashMap<>(roles.size()); result.put(user.getName(), true); - tenantsMM.entries().stream().filter(e -> roles.contains(e.getKey())).filter(e -> !user.getName().equals(e.getValue().v1())).forEach(e -> { - - // replaceProperties for tenant name because - // at this point e.getValue().v1() can be in this form : "${attr.[internal|jwt|proxy|ldap].*}" - // let's substitute it with the eventual value of the user's attribute - final String tenant = replaceProperties(e.getValue().v1(),user); - final boolean rw = e.getValue().v2(); - - if (rw || !result.containsKey(tenant)) { //RW outperforms RO - - // We want to make sure that we add a tenant that exists - // Indeed, because we don't have control over what will be - // passed on as values of users' attributes, we have to make - // sure that we don't allow them to select tenants that do not exist. - if(ConfigModelV7.this.tenants.getCEntries().containsKey(tenant)) { - result.put(tenant, rw); + tenantsMM.entries() + .stream() + .filter(e -> roles.contains(e.getKey())) + .filter(e -> !user.getName().equals(e.getValue().v1())) + .forEach(e -> { + + // replaceProperties for tenant name because + // at this point e.getValue().v1() can be in this form : "${attr.[internal|jwt|proxy|ldap].*}" + // let's substitute it with the eventual value of the user's attribute + final String tenant = replaceProperties(e.getValue().v1(), user); + final boolean rw = e.getValue().v2(); + + if (rw || !result.containsKey(tenant)) { // RW outperforms RO + + // We want to make sure that we add a tenant that exists + // Indeed, because we don't have control over what will be + // passed on as values of users' attributes, we have to make + // sure that we don't allow them to select tenants that do not exist. + if (ConfigModelV7.this.tenants.getCEntries().containsKey(tenant)) { + result.put(tenant, rw); + } } - } - }); + }); Set _roles = new TreeSet<>(String.CASE_INSENSITIVE_ORDER); _roles.addAll(roles); - if(!result.containsKey("global_tenant") && ( - _roles.contains("kibana_user") - || _roles.contains("all_access") - )) { + if (!result.containsKey("global_tenant") && (_roles.contains("kibana_user") || _roles.contains("all_access"))) { result.put("global_tenant", true); } @@ -1212,7 +1267,7 @@ private Set map(final User user, final TransportAddress caller) { final Set securityRoles = new HashSet<>(user.getSecurityRoles()); if (rolesMappingResolution == ConfigConstants.RolesMappingResolution.BOTH - || rolesMappingResolution == ConfigConstants.RolesMappingResolution.BACKENDROLES_ONLY) { + || rolesMappingResolution == ConfigConstants.RolesMappingResolution.BACKENDROLES_ONLY) { if (log.isDebugEnabled()) { log.debug("Pass backendroles from {}", user); } @@ -1220,7 +1275,7 @@ private Set map(final User user, final TransportAddress caller) { } if (((rolesMappingResolution == ConfigConstants.RolesMappingResolution.BOTH - || rolesMappingResolution == ConfigConstants.RolesMappingResolution.MAPPING_ONLY))) { + || rolesMappingResolution == ConfigConstants.RolesMappingResolution.MAPPING_ONLY))) { for (String p : WildcardMatcher.getAllMatchingPatterns(userMatchers, user.getName())) { securityRoles.addAll(users.get(p)); @@ -1236,7 +1291,7 @@ private Set map(final User user, final TransportAddress caller) { } if (caller != null) { - //IPV4 or IPv6 (compressed and without scope identifiers) + // IPV4 or IPv6 (compressed and without scope identifiers) final String ipAddress = caller.getAddress(); for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, ipAddress)) { @@ -1244,7 +1299,7 @@ private Set map(final User user, final TransportAddress caller) { } if (caller.address() != null - && (hostResolverMode.equalsIgnoreCase("ip-hostname") || hostResolverMode.equalsIgnoreCase("ip-hostname-lookup"))) { + && (hostResolverMode.equalsIgnoreCase("ip-hostname") || hostResolverMode.equalsIgnoreCase("ip-hostname-lookup"))) { final String hostName = caller.address().getHostString(); for (String p : WildcardMatcher.getAllMatchingPatterns(hostMatchers, hostName)) { @@ -1268,10 +1323,6 @@ private Set map(final User user, final TransportAddress caller) { } } - - - - public Map mapTenants(User user, Set roles) { return tenantHolder.mapTenants(user, roles); } diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java index 9d8c36576c..65be27d64f 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigFactory.java @@ -89,29 +89,32 @@ static void resetStatics() { } private void loadStaticConfig() throws IOException { - JsonNode staticRolesJsonNode = DefaultObjectMapper.YAML_MAPPER - .readTree(DynamicConfigFactory.class.getResourceAsStream("/static_config/static_roles.yml")); + JsonNode staticRolesJsonNode = DefaultObjectMapper.YAML_MAPPER.readTree( + DynamicConfigFactory.class.getResourceAsStream("/static_config/static_roles.yml") + ); staticRoles = SecurityDynamicConfiguration.fromNode(staticRolesJsonNode, CType.ROLES, 2, 0, 0); - JsonNode staticActionGroupsJsonNode = DefaultObjectMapper.YAML_MAPPER - .readTree(DynamicConfigFactory.class.getResourceAsStream("/static_config/static_action_groups.yml")); + JsonNode staticActionGroupsJsonNode = DefaultObjectMapper.YAML_MAPPER.readTree( + DynamicConfigFactory.class.getResourceAsStream("/static_config/static_action_groups.yml") + ); staticActionGroups = SecurityDynamicConfiguration.fromNode(staticActionGroupsJsonNode, CType.ACTIONGROUPS, 2, 0, 0); - JsonNode staticTenantsJsonNode = DefaultObjectMapper.YAML_MAPPER - .readTree(DynamicConfigFactory.class.getResourceAsStream("/static_config/static_tenants.yml")); + JsonNode staticTenantsJsonNode = DefaultObjectMapper.YAML_MAPPER.readTree( + DynamicConfigFactory.class.getResourceAsStream("/static_config/static_tenants.yml") + ); staticTenants = SecurityDynamicConfiguration.fromNode(staticTenantsJsonNode, CType.TENANTS, 2, 0, 0); } public final static SecurityDynamicConfiguration addStatics(SecurityDynamicConfiguration original) { - if(original.getCType() == CType.ACTIONGROUPS && !staticActionGroups.getCEntries().isEmpty()) { + if (original.getCType() == CType.ACTIONGROUPS && !staticActionGroups.getCEntries().isEmpty()) { original.add(staticActionGroups.deepClone()); } - if(original.getCType() == CType.ROLES && !staticRoles.getCEntries().isEmpty()) { + if (original.getCType() == CType.ROLES && !staticRoles.getCEntries().isEmpty()) { original.add(staticRoles.deepClone()); } - if(original.getCType() == CType.TENANTS && !staticTenants.getCEntries().isEmpty()) { + if (original.getCType() == CType.TENANTS && !staticTenants.getCEntries().isEmpty()) { original.add(staticTenants.deepClone()); } @@ -128,18 +131,24 @@ public final static SecurityDynamicConfiguration addStatics(SecurityDynamicCo SecurityDynamicConfiguration config; - public DynamicConfigFactory(ConfigurationRepository cr, final Settings opensearchSettings, - final Path configPath, Client client, ThreadPool threadPool, ClusterInfoHolder cih) { + public DynamicConfigFactory( + ConfigurationRepository cr, + final Settings opensearchSettings, + final Path configPath, + Client client, + ThreadPool threadPool, + ClusterInfoHolder cih + ) { super(); this.cr = cr; this.opensearchSettings = opensearchSettings; this.configPath = configPath; - if(opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES, true)) { + if (opensearchSettings.getAsBoolean(ConfigConstants.SECURITY_UNSUPPORTED_LOAD_STATIC_RESOURCES, true)) { try { loadStaticConfig(); } catch (IOException e) { - throw new StaticResourceException("Unable to load static resources due to "+e, e); + throw new StaticResourceException("Unable to load static resources due to " + e, e); } } else { log.info("Static resources will not be loaded."); @@ -162,18 +171,55 @@ public void onChange(Map> typeToConfig) { SecurityDynamicConfiguration whitelistingSetting = cr.getConfiguration(CType.WHITELIST); SecurityDynamicConfiguration allowlistingSetting = cr.getConfiguration(CType.ALLOWLIST); - if (log.isDebugEnabled()) { - String logmsg = "current config (because of " + typeToConfig.keySet() + ")\n" + - " actionGroups: " + actionGroups.getImplementingClass() + " with " + actionGroups.getCEntries().size() + " entries\n" + - " config: " + config.getImplementingClass() + " with " + config.getCEntries().size() + " entries\n" + - " internalusers: " + internalusers.getImplementingClass() + " with " + internalusers.getCEntries().size() + " entries\n" + - " roles: " + roles.getImplementingClass() + " with " + roles.getCEntries().size() + " entries\n" + - " rolesmapping: " + rolesmapping.getImplementingClass() + " with " + rolesmapping.getCEntries().size() + " entries\n" + - " tenants: " + tenants.getImplementingClass() + " with " + tenants.getCEntries().size() + " entries\n" + - " nodesdn: " + nodesDn.getImplementingClass() + " with " + nodesDn.getCEntries().size() + " entries\n" + - " whitelist " + whitelistingSetting.getImplementingClass() + " with " + whitelistingSetting.getCEntries().size() + " entries\n" + - " allowlist " + allowlistingSetting.getImplementingClass() + " with " + allowlistingSetting.getCEntries().size() + " entries\n"; + String logmsg = "current config (because of " + + typeToConfig.keySet() + + ")\n" + + " actionGroups: " + + actionGroups.getImplementingClass() + + " with " + + actionGroups.getCEntries().size() + + " entries\n" + + " config: " + + config.getImplementingClass() + + " with " + + config.getCEntries().size() + + " entries\n" + + " internalusers: " + + internalusers.getImplementingClass() + + " with " + + internalusers.getCEntries().size() + + " entries\n" + + " roles: " + + roles.getImplementingClass() + + " with " + + roles.getCEntries().size() + + " entries\n" + + " rolesmapping: " + + rolesmapping.getImplementingClass() + + " with " + + rolesmapping.getCEntries().size() + + " entries\n" + + " tenants: " + + tenants.getImplementingClass() + + " with " + + tenants.getCEntries().size() + + " entries\n" + + " nodesdn: " + + nodesDn.getImplementingClass() + + " with " + + nodesDn.getCEntries().size() + + " entries\n" + + " whitelist " + + whitelistingSetting.getImplementingClass() + + " with " + + whitelistingSetting.getCEntries().size() + + " entries\n" + + " allowlist " + + allowlistingSetting.getImplementingClass() + + " with " + + allowlistingSetting.getCEntries().size() + + " entries\n"; log.debug(logmsg); } @@ -183,70 +229,85 @@ public void onChange(Map> typeToConfig) { final NodesDnModel nm = new NodesDnModelImpl(nodesDn); final WhitelistingSettings whitelist = (WhitelistingSettings) cr.getConfiguration(CType.WHITELIST).getCEntry("config"); final AllowlistingSettings allowlist = (AllowlistingSettings) cr.getConfiguration(CType.ALLOWLIST).getCEntry("config"); - final AuditConfig audit = (AuditConfig)cr.getConfiguration(CType.AUDIT).getCEntry("config"); - - if(config.getImplementingClass() == ConfigV7.class) { - //statics - - if(roles.containsAny(staticRoles)) { - throw new StaticResourceException("Cannot override static roles"); - } - if(!roles.add(staticRoles) && !staticRoles.getCEntries().isEmpty()) { - throw new StaticResourceException("Unable to load static roles"); - } - - log.debug("Static roles loaded ({})", staticRoles.getCEntries().size()); + final AuditConfig audit = (AuditConfig) cr.getConfiguration(CType.AUDIT).getCEntry("config"); - if(actionGroups.containsAny(staticActionGroups)) { - System.out.println("static: " + actionGroups.getCEntries()); - System.out.println("Static Action Groups:" + staticActionGroups.getCEntries()); - throw new StaticResourceException("Cannot override static action groups"); - } - if(!actionGroups.add(staticActionGroups) && !staticActionGroups.getCEntries().isEmpty()) { - throw new StaticResourceException("Unable to load static action groups"); - } + if (config.getImplementingClass() == ConfigV7.class) { + // statics + if (roles.containsAny(staticRoles)) { + throw new StaticResourceException("Cannot override static roles"); + } + if (!roles.add(staticRoles) && !staticRoles.getCEntries().isEmpty()) { + throw new StaticResourceException("Unable to load static roles"); + } - log.debug("Static action groups loaded ({})", staticActionGroups.getCEntries().size()); - - if(tenants.containsAny(staticTenants)) { - throw new StaticResourceException("Cannot override static tenants"); - } - if(!tenants.add(staticTenants) && !staticTenants.getCEntries().isEmpty()) { - throw new StaticResourceException("Unable to load static tenants"); - } + log.debug("Static roles loaded ({})", staticRoles.getCEntries().size()); + if (actionGroups.containsAny(staticActionGroups)) { + System.out.println("static: " + actionGroups.getCEntries()); + System.out.println("Static Action Groups:" + staticActionGroups.getCEntries()); + throw new StaticResourceException("Cannot override static action groups"); + } + if (!actionGroups.add(staticActionGroups) && !staticActionGroups.getCEntries().isEmpty()) { + throw new StaticResourceException("Unable to load static action groups"); + } - log.debug("Static tenants loaded ({})", staticTenants.getCEntries().size()); + log.debug("Static action groups loaded ({})", staticActionGroups.getCEntries().size()); - log.debug("Static configuration loaded (total roles: {}/total action groups: {}/total tenants: {})", - roles.getCEntries().size(), actionGroups.getCEntries().size(), tenants.getCEntries().size()); + if (tenants.containsAny(staticTenants)) { + throw new StaticResourceException("Cannot override static tenants"); + } + if (!tenants.add(staticTenants) && !staticTenants.getCEntries().isEmpty()) { + throw new StaticResourceException("Unable to load static tenants"); + } + log.debug("Static tenants loaded ({})", staticTenants.getCEntries().size()); + log.debug( + "Static configuration loaded (total roles: {}/total action groups: {}/total tenants: {})", + roles.getCEntries().size(), + actionGroups.getCEntries().size(), + tenants.getCEntries().size() + ); - //rebuild v7 Models + // rebuild v7 Models dcm = new DynamicConfigModelV7(getConfigV7(config), opensearchSettings, configPath, iab); - ium = new InternalUsersModelV7((SecurityDynamicConfiguration) internalusers, + ium = new InternalUsersModelV7( + (SecurityDynamicConfiguration) internalusers, + (SecurityDynamicConfiguration) roles, + (SecurityDynamicConfiguration) rolesmapping + ); + cm = new ConfigModelV7( (SecurityDynamicConfiguration) roles, - (SecurityDynamicConfiguration) rolesmapping); - cm = new ConfigModelV7((SecurityDynamicConfiguration) roles,(SecurityDynamicConfiguration)rolesmapping, (SecurityDynamicConfiguration)actionGroups, (SecurityDynamicConfiguration) tenants,dcm, opensearchSettings); + (SecurityDynamicConfiguration) rolesmapping, + (SecurityDynamicConfiguration) actionGroups, + (SecurityDynamicConfiguration) tenants, + dcm, + opensearchSettings + ); } else { - //rebuild v6 Models + // rebuild v6 Models dcm = new DynamicConfigModelV6(getConfigV6(config), opensearchSettings, configPath, iab); ium = new InternalUsersModelV6((SecurityDynamicConfiguration) internalusers); - cm = new ConfigModelV6((SecurityDynamicConfiguration) roles, (SecurityDynamicConfiguration)actionGroups, (SecurityDynamicConfiguration)rolesmapping, dcm, opensearchSettings); + cm = new ConfigModelV6( + (SecurityDynamicConfiguration) roles, + (SecurityDynamicConfiguration) actionGroups, + (SecurityDynamicConfiguration) rolesmapping, + dcm, + opensearchSettings + ); } - //notify subscribers + // notify subscribers eventBus.post(cm); eventBus.post(dcm); eventBus.post(ium); eventBus.post(nm); - eventBus.post(whitelist==null? defaultWhitelistingSettings: whitelist); - eventBus.post(allowlist==null? defaultAllowlistingSettings: allowlist); + eventBus.post(whitelist == null ? defaultWhitelistingSettings : whitelist); + eventBus.post(allowlist == null ? defaultAllowlistingSettings : allowlist); if (cr.isAuditHotReloadingEnabled()) { eventBus.post(audit); } @@ -288,9 +349,11 @@ private static class InternalUsersModelV7 extends InternalUsersModel { private final SecurityDynamicConfiguration rolesMappingsV7SecurityDynamicConfiguration; - public InternalUsersModelV7(SecurityDynamicConfiguration internalUserV7SecurityDynamicConfiguration, - SecurityDynamicConfiguration rolesV7SecurityDynamicConfiguration, - SecurityDynamicConfiguration rolesMappingsV7SecurityDynamicConfiguration) { + public InternalUsersModelV7( + SecurityDynamicConfiguration internalUserV7SecurityDynamicConfiguration, + SecurityDynamicConfiguration rolesV7SecurityDynamicConfiguration, + SecurityDynamicConfiguration rolesMappingsV7SecurityDynamicConfiguration + ) { super(); this.internalUserV7SecurityDynamicConfiguration = internalUserV7SecurityDynamicConfiguration; this.rolesV7SecurityDynamicConfiguration = rolesV7SecurityDynamicConfiguration; @@ -305,25 +368,25 @@ public boolean exists(String user) { @Override public List getBackenRoles(String user) { InternalUserV7 tmp = internalUserV7SecurityDynamicConfiguration.getCEntry(user); - return tmp==null?null:tmp.getBackend_roles(); + return tmp == null ? null : tmp.getBackend_roles(); } @Override public Map getAttributes(String user) { InternalUserV7 tmp = internalUserV7SecurityDynamicConfiguration.getCEntry(user); - return tmp==null?null:tmp.getAttributes(); + return tmp == null ? null : tmp.getAttributes(); } @Override public String getDescription(String user) { InternalUserV7 tmp = internalUserV7SecurityDynamicConfiguration.getCEntry(user); - return tmp==null?null:tmp.getDescription(); + return tmp == null ? null : tmp.getDescription(); } @Override public String getHash(String user) { InternalUserV7 tmp = internalUserV7SecurityDynamicConfiguration.getCEntry(user); - return tmp==null?null:tmp.getHash(); + return tmp == null ? null : tmp.getHash(); } public List getSecurityRoles(String user) { @@ -331,14 +394,18 @@ public List getSecurityRoles(String user) { // Security roles should only contain roles that exist in the roles dynamic config. // We should filter out any roles that have hidden rolesmapping. - return tmp == null ? ImmutableList.of() : - tmp.getOpendistro_security_roles().stream().filter(role -> !isRolesMappingHidden(role) && rolesV7SecurityDynamicConfiguration.exists(role)).collect(ImmutableList.toImmutableList()); + return tmp == null + ? ImmutableList.of() + : tmp.getOpendistro_security_roles() + .stream() + .filter(role -> !isRolesMappingHidden(role) && rolesV7SecurityDynamicConfiguration.exists(role)) + .collect(ImmutableList.toImmutableList()); } // Remove any hidden rolesmapping from the security roles private boolean isRolesMappingHidden(String rolename) { final RoleMappingsV7 roleMapping = rolesMappingsV7SecurityDynamicConfiguration.getCEntry(rolename); - return roleMapping!=null && roleMapping.isHidden(); + return roleMapping != null && roleMapping.isHidden(); } } @@ -346,7 +413,6 @@ private static class InternalUsersModelV6 extends InternalUsersModel { SecurityDynamicConfiguration configuration; - public InternalUsersModelV6(SecurityDynamicConfiguration configuration) { super(); this.configuration = configuration; @@ -360,13 +426,13 @@ public boolean exists(String user) { @Override public List getBackenRoles(String user) { InternalUserV6 tmp = configuration.getCEntry(user); - return tmp==null?null:tmp.getRoles(); + return tmp == null ? null : tmp.getRoles(); } @Override public Map getAttributes(String user) { InternalUserV6 tmp = configuration.getCEntry(user); - return tmp==null?null:tmp.getAttributes(); + return tmp == null ? null : tmp.getAttributes(); } @Override @@ -377,7 +443,7 @@ public String getDescription(String user) { @Override public String getHash(String user) { InternalUserV6 tmp = configuration.getCEntry(user); - return tmp==null?null:tmp.getHash(); + return tmp == null ? null : tmp.getHash(); } public List getSecurityRoles(String user) { @@ -391,14 +457,17 @@ private static class NodesDnModelImpl extends NodesDnModel { public NodesDnModelImpl(SecurityDynamicConfiguration configuration) { super(); - this.configuration = null == configuration.getCType() ? SecurityDynamicConfiguration.empty() : - (SecurityDynamicConfiguration)configuration; + this.configuration = null == configuration.getCType() + ? SecurityDynamicConfiguration.empty() + : (SecurityDynamicConfiguration) configuration; } @Override public Map getNodesDn() { - return this.configuration.getCEntries().entrySet().stream().collect( - ImmutableMap.toImmutableMap(Entry::getKey, entry -> WildcardMatcher.from(entry.getValue().getNodesDn(), false))); + return this.configuration.getCEntries() + .entrySet() + .stream() + .collect(ImmutableMap.toImmutableMap(Entry::getKey, entry -> WildcardMatcher.from(entry.getValue().getNodesDn(), false))); } } } diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java index 959679cf85..e3d10878da 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModel.java @@ -56,31 +56,55 @@ public abstract class DynamicConfigModel { protected final Logger log = LogManager.getLogger(this.getClass()); + public abstract SortedSet getRestAuthDomains(); + public abstract Set getRestAuthorizers(); + public abstract boolean isAnonymousAuthenticationEnabled(); + public abstract boolean isXffEnabled(); + public abstract String getInternalProxies(); + public abstract String getRemoteIpHeader(); + public abstract boolean isRestAuthDisabled(); + public abstract boolean isInterTransportAuthDisabled(); + public abstract boolean isRespectRequestIndicesEnabled(); + public abstract String getDashboardsServerUsername(); + public abstract String getDashboardsOpenSearchRole(); + public abstract String getDashboardsIndexname(); + public abstract boolean isDashboardsMultitenancyEnabled(); + public abstract boolean isDashboardsPrivateTenantEnabled(); + public abstract String getDashboardsDefaultTenant(); + public abstract boolean isDnfofEnabled(); + public abstract boolean isMultiRolespanEnabled(); + public abstract String getFilteredAliasMode(); + public abstract String getHostsResolverMode(); + public abstract boolean isDnfofForEmptyResultsEnabled(); public abstract List getIpAuthFailureListeners(); + public abstract Multimap getAuthBackendFailureListeners(); + public abstract List> getIpClientBlockRegistries(); + public abstract Multimap> getAuthBackendClientBlockRegistries(); + public abstract Settings getDynamicOnBehalfOfSettings(); protected final Map authImplMap = new HashMap<>(); @@ -116,6 +140,4 @@ public DynamicConfigModel() { authImplMap.put("username_authFailureListener", UserNameBasedRateLimiter.class.getName()); } - - } diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java index 3255bb4de4..e5308aa574 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV6.java @@ -81,27 +81,32 @@ public class DynamicConfigModelV6 extends DynamicConfigModel { public DynamicConfigModelV6(ConfigV6 config, Settings opensearchSettings, Path configPath, InternalAuthenticationBackend iab) { super(); this.config = config; - this.opensearchSettings = opensearchSettings; + this.opensearchSettings = opensearchSettings; this.configPath = configPath; this.iab = iab; buildAAA(); } + @Override public SortedSet getRestAuthDomains() { return Collections.unmodifiableSortedSet(restAuthDomains); } + @Override public Set getRestAuthorizers() { return Collections.unmodifiableSet(restAuthorizers); } + @Override public boolean isAnonymousAuthenticationEnabled() { return config.dynamic.http.anonymous_auth_enabled; } + @Override public boolean isXffEnabled() { return config.dynamic.http.xff.enabled; } + @Override public String getInternalProxies() { return config.dynamic.http.xff.internalProxies; @@ -116,44 +121,57 @@ public String getRemoteIpHeader() { public boolean isRestAuthDisabled() { return config.dynamic.disable_rest_auth; } + @Override public boolean isInterTransportAuthDisabled() { return config.dynamic.disable_intertransport_auth; } + @Override public boolean isRespectRequestIndicesEnabled() { return config.dynamic.respect_request_indices_options; } + @Override public String getDashboardsServerUsername() { return config.dynamic.kibana.server_username; } + @Override public String getDashboardsOpenSearchRole() { return config.dynamic.kibana.opendistro_role; } + @Override public String getDashboardsIndexname() { return config.dynamic.kibana.index; } + @Override public boolean isDashboardsMultitenancyEnabled() { return config.dynamic.kibana.multitenancy_enabled; } + @Override public boolean isDashboardsPrivateTenantEnabled() { return config.dynamic.kibana.private_tenant_enabled; } + @Override - public String getDashboardsDefaultTenant() { return config.dynamic.kibana.default_tenant; } + public String getDashboardsDefaultTenant() { + return config.dynamic.kibana.default_tenant; + } + @Override public boolean isDnfofEnabled() { return config.dynamic.do_not_fail_on_forbidden || config.dynamic.kibana.do_not_fail_on_forbidden; } + @Override public boolean isMultiRolespanEnabled() { return config.dynamic.multi_rolespan_enabled; } + @Override public String getFilteredAliasMode() { return config.dynamic.filtered_alias_mode; @@ -213,26 +231,33 @@ private void buildAAA() { final boolean httpEnabled = enabled && ad.getValue().http_enabled; final boolean transportEnabled = enabled && ad.getValue().transport_enabled; - if (httpEnabled || transportEnabled) { try { final String authzBackendClazz = ad.getValue().authorization_backend.type; final AuthorizationBackend authorizationBackend; - if(authzBackendClazz.equals(InternalAuthenticationBackend.class.getName()) //NOSONAR - || authzBackendClazz.equals("internal") - || authzBackendClazz.equals("intern")) { + 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() + authzBackendClazz, + "z", + Settings.builder() .put(opensearchSettings) - //.putProperties(ads.getAsStringMap(DotPath.of("authorization_backend.config")), DynamicConfiguration.checkKeyFunction()).build(), configPath); - .put(Settings.builder().loadFromSource(ad.getValue().authorization_backend.configAsJson(), XContentType.JSON).build()).build() - , configPath); + // .putProperties(ads.getAsStringMap(DotPath.of("authorization_backend.config")), + // DynamicConfiguration.checkKeyFunction()).build(), configPath); + .put( + Settings.builder() + .loadFromSource(ad.getValue().authorization_backend.configAsJson(), XContentType.JSON) + .build() + ) + .build(), + configPath + ); } if (httpEnabled) { @@ -247,7 +272,7 @@ private void buildAAA() { destroyableComponents0.add((Destroyable) authorizationBackend); } } catch (final Exception e) { - log.error("Unable to initialize AuthorizationBackend {} due to {}", ad, e.toString(),e); + log.error("Unable to initialize AuthorizationBackend {} due to {}", ad, e.toString(), e); } } } @@ -263,31 +288,56 @@ private void buildAAA() { try { AuthenticationBackend authenticationBackend; final String authBackendClazz = ad.getValue().authentication_backend.type; - if(authBackendClazz.equals(InternalAuthenticationBackend.class.getName()) //NOSONAR - || authBackendClazz.equals("internal") - || authBackendClazz.equals("intern")) { + 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() + authBackendClazz, + "c", + Settings.builder() .put(opensearchSettings) - //.putProperties(ads.getAsStringMap(DotPath.of("authentication_backend.config")), DynamicConfiguration.checkKeyFunction()).build() - .put(Settings.builder().loadFromSource(ad.getValue().authentication_backend.configAsJson(), XContentType.JSON).build()).build() - , configPath); + // .putProperties(ads.getAsStringMap(DotPath.of("authentication_backend.config")), + // DynamicConfiguration.checkKeyFunction()).build() + .put( + Settings.builder() + .loadFromSource(ad.getValue().authentication_backend.configAsJson(), XContentType.JSON) + .build() + ) + .build(), + configPath + ); } - String httpAuthenticatorType = ad.getValue().http_authenticator.type; //no default - HTTPAuthenticator httpAuthenticator = httpAuthenticatorType==null?null: (HTTPAuthenticator) newInstance(httpAuthenticatorType,"h", - Settings.builder().put(opensearchSettings) - //.putProperties(ads.getAsStringMap(DotPath.of("http_authenticator.config")), DynamicConfiguration.checkKeyFunction()).build(), - .put(Settings.builder().loadFromSource(ad.getValue().http_authenticator.configAsJson(), XContentType.JSON).build()).build() - - , configPath); - - final AuthDomain _ad = new AuthDomain(authenticationBackend, httpAuthenticator, - ad.getValue().http_authenticator.challenge, ad.getValue().order); + String httpAuthenticatorType = ad.getValue().http_authenticator.type; // no default + HTTPAuthenticator httpAuthenticator = httpAuthenticatorType == null + ? null + : (HTTPAuthenticator) newInstance( + httpAuthenticatorType, + "h", + Settings.builder() + .put(opensearchSettings) + // .putProperties(ads.getAsStringMap(DotPath.of("http_authenticator.config")), + // DynamicConfiguration.checkKeyFunction()).build(), + .put( + Settings.builder() + .loadFromSource(ad.getValue().http_authenticator.configAsJson(), XContentType.JSON) + .build() + ) + .build() + + , + configPath + ); + + final AuthDomain _ad = new AuthDomain( + authenticationBackend, + httpAuthenticator, + ad.getValue().http_authenticator.challenge, + ad.getValue().order + ); if (httpEnabled && _ad.getHttpAuthenticator() != null) { restAuthDomains0.add(_ad); @@ -321,14 +371,19 @@ private void buildAAA() { destroyableComponents = Collections.unmodifiableList(destroyableComponents0); - if(originalDestroyableComponents != null) { + if (originalDestroyableComponents != null) { destroyDestroyables(originalDestroyableComponents); } originalDestroyableComponents = null; - createAuthFailureListeners(ipAuthFailureListeners0, - authBackendFailureListeners0, ipClientBlockRegistries0, authBackendClientBlockRegistries0, destroyableComponents0); + createAuthFailureListeners( + ipAuthFailureListeners0, + authBackendFailureListeners0, + ipClientBlockRegistries0, + authBackendClientBlockRegistries0, + destroyableComponents0 + ); ipAuthFailureListeners = Collections.unmodifiableList(ipAuthFailureListeners0); ipClientBlockRegistries = Collections.unmodifiableList(ipClientBlockRegistries0); @@ -351,8 +406,8 @@ private T newInstance(final String clazzOrShortcut, String type, final Setti String clazz = clazzOrShortcut; - if(authImplMap.containsKey(clazz+"_"+type)) { - clazz = authImplMap.get(clazz+"_"+type); + if (authImplMap.containsKey(clazz + "_" + type)) { + clazz = authImplMap.get(clazz + "_" + type); } return ReflectionHelper.instantiateAAA(clazz, settings, configPath); @@ -367,15 +422,20 @@ private String translateShortcutToClassName(final String clazzOrShortcut, final } } - private void createAuthFailureListeners(List ipAuthFailureListeners, - Multimap authBackendFailureListeners, List> ipClientBlockRegistries, - Multimap> authBackendUserClientBlockRegistries, List destroyableComponents0) { + private void createAuthFailureListeners( + List ipAuthFailureListeners, + Multimap authBackendFailureListeners, + List> ipClientBlockRegistries, + Multimap> authBackendUserClientBlockRegistries, + List destroyableComponents0 + ) { for (Entry entry : config.dynamic.auth_failure_listeners.getListeners().entrySet()) { Settings entrySettings = Settings.builder() - .put(opensearchSettings) - .put(Settings.builder().loadFromSource(entry.getValue().asJson(), XContentType.JSON).build()).build(); + .put(opensearchSettings) + .put(Settings.builder().loadFromSource(entry.getValue().asJson(), XContentType.JSON).build()) + .build(); String type = entry.getValue().type; String authenticationBackend = entry.getValue().authentication_backend; @@ -392,8 +452,13 @@ private void createAuthFailureListeners(List ipAuthFailureL ipClientBlockRegistries.add(clientBlockRegistry); } else { - log.error("Illegal ClientIdType for AuthFailureListener" + entry.getKey() + ": " - + ((ClientBlockRegistry) authFailureListener).getClientIdType() + "; must be InetAddress."); + log.error( + "Illegal ClientIdType for AuthFailureListener" + + entry.getKey() + + ": " + + ((ClientBlockRegistry) authFailureListener).getClientIdType() + + "; must be InetAddress." + ); } } @@ -410,8 +475,13 @@ private void createAuthFailureListeners(List ipAuthFailureL authBackendUserClientBlockRegistries.put(authenticationBackend, clientBlockRegistry); } else { - log.error("Illegal ClientIdType for AuthFailureListener" + entry.getKey() + ": " - + ((ClientBlockRegistry) authFailureListener).getClientIdType() + "; must be InetAddress."); + log.error( + "Illegal ClientIdType for AuthFailureListener" + + entry.getKey() + + ": " + + ((ClientBlockRegistry) authFailureListener).getClientIdType() + + "; must be InetAddress." + ); } } } diff --git a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java index 0b66d9a306..a386e70093 100644 --- a/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java +++ b/src/main/java/org/opensearch/security/securityconf/DynamicConfigModelV7.java @@ -81,27 +81,32 @@ public class DynamicConfigModelV7 extends DynamicConfigModel { public DynamicConfigModelV7(ConfigV7 config, Settings opensearchSettings, Path configPath, InternalAuthenticationBackend iab) { super(); this.config = config; - this.opensearchSettings = opensearchSettings; + this.opensearchSettings = opensearchSettings; this.configPath = configPath; this.iab = iab; buildAAA(); } + @Override public SortedSet getRestAuthDomains() { return Collections.unmodifiableSortedSet(restAuthDomains); } + @Override public Set getRestAuthorizers() { return Collections.unmodifiableSet(restAuthorizers); } + @Override public boolean isAnonymousAuthenticationEnabled() { return config.dynamic.http.anonymous_auth_enabled; } + @Override public boolean isXffEnabled() { return config.dynamic.http.xff.enabled; } + @Override public String getInternalProxies() { return config.dynamic.http.xff.internalProxies; @@ -116,44 +121,57 @@ public String getRemoteIpHeader() { public boolean isRestAuthDisabled() { return config.dynamic.disable_rest_auth; } + @Override public boolean isInterTransportAuthDisabled() { return config.dynamic.disable_intertransport_auth; } + @Override public boolean isRespectRequestIndicesEnabled() { return config.dynamic.respect_request_indices_options; } + @Override public String getDashboardsServerUsername() { return config.dynamic.kibana.server_username; } + @Override public String getDashboardsOpenSearchRole() { return config.dynamic.kibana.opendistro_role; } + @Override public String getDashboardsIndexname() { return config.dynamic.kibana.index; } + @Override public boolean isDashboardsMultitenancyEnabled() { return config.dynamic.kibana.multitenancy_enabled; } + @Override public boolean isDashboardsPrivateTenantEnabled() { return config.dynamic.kibana.private_tenant_enabled; } + @Override - public String getDashboardsDefaultTenant() { return config.dynamic.kibana.default_tenant; } + public String getDashboardsDefaultTenant() { + return config.dynamic.kibana.default_tenant; + } + @Override public boolean isDnfofEnabled() { return config.dynamic.do_not_fail_on_forbidden; } + @Override public boolean isMultiRolespanEnabled() { return config.dynamic.multi_rolespan_enabled; } + @Override public String getFilteredAliasMode() { return config.dynamic.filtered_alias_mode; @@ -192,8 +210,8 @@ public Multimap> getAuthBackendClientBlockRe @Override public Settings getDynamicOnBehalfOfSettings() { return Settings.builder() - .put(Settings.builder().loadFromSource(config.dynamic.on_behalf_of.configAsJson(), XContentType.JSON).build()) - .build(); + .put(Settings.builder().loadFromSource(config.dynamic.on_behalf_of.configAsJson(), XContentType.JSON).build()) + .build(); } private void buildAAA() { @@ -214,26 +232,33 @@ private void buildAAA() { final boolean httpEnabled = ad.getValue().http_enabled; final boolean transportEnabled = ad.getValue().transport_enabled; - if (httpEnabled || transportEnabled) { try { final String authzBackendClazz = ad.getValue().authorization_backend.type; final AuthorizationBackend authorizationBackend; - if(authzBackendClazz.equals(InternalAuthenticationBackend.class.getName()) //NOSONAR - || authzBackendClazz.equals("internal") - || authzBackendClazz.equals("intern")) { + 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() + authzBackendClazz, + "z", + Settings.builder() .put(opensearchSettings) - //.putProperties(ads.getAsStringMap(DotPath.of("authorization_backend.config")), DynamicConfiguration.checkKeyFunction()).build(), configPath); - .put(Settings.builder().loadFromSource(ad.getValue().authorization_backend.configAsJson(), XContentType.JSON).build()).build() - , configPath); + // .putProperties(ads.getAsStringMap(DotPath.of("authorization_backend.config")), + // DynamicConfiguration.checkKeyFunction()).build(), configPath); + .put( + Settings.builder() + .loadFromSource(ad.getValue().authorization_backend.configAsJson(), XContentType.JSON) + .build() + ) + .build(), + configPath + ); } if (httpEnabled) { @@ -248,7 +273,7 @@ private void buildAAA() { destroyableComponents0.add((Destroyable) authorizationBackend); } } catch (final Exception e) { - log.error("Unable to initialize AuthorizationBackend {} due to {}", ad, e.toString(),e); + log.error("Unable to initialize AuthorizationBackend {} due to {}", ad, e.toString(), e); } } } @@ -263,31 +288,56 @@ private void buildAAA() { try { AuthenticationBackend authenticationBackend; final String authBackendClazz = ad.getValue().authentication_backend.type; - if(authBackendClazz.equals(InternalAuthenticationBackend.class.getName()) //NOSONAR - || authBackendClazz.equals("internal") - || authBackendClazz.equals("intern")) { + 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() + authBackendClazz, + "c", + Settings.builder() .put(opensearchSettings) - //.putProperties(ads.getAsStringMap(DotPath.of("authentication_backend.config")), DynamicConfiguration.checkKeyFunction()).build() - .put(Settings.builder().loadFromSource(ad.getValue().authentication_backend.configAsJson(), XContentType.JSON).build()).build() - , configPath); + // .putProperties(ads.getAsStringMap(DotPath.of("authentication_backend.config")), + // DynamicConfiguration.checkKeyFunction()).build() + .put( + Settings.builder() + .loadFromSource(ad.getValue().authentication_backend.configAsJson(), XContentType.JSON) + .build() + ) + .build(), + configPath + ); } - String httpAuthenticatorType = ad.getValue().http_authenticator.type; //no default - HTTPAuthenticator httpAuthenticator = httpAuthenticatorType==null?null: (HTTPAuthenticator) newInstance(httpAuthenticatorType,"h", - Settings.builder().put(opensearchSettings) - //.putProperties(ads.getAsStringMap(DotPath.of("http_authenticator.config")), DynamicConfiguration.checkKeyFunction()).build(), - .put(Settings.builder().loadFromSource(ad.getValue().http_authenticator.configAsJson(), XContentType.JSON).build()).build() - - , configPath); - - final AuthDomain _ad = new AuthDomain(authenticationBackend, httpAuthenticator, - ad.getValue().http_authenticator.challenge, ad.getValue().order); + String httpAuthenticatorType = ad.getValue().http_authenticator.type; // no default + HTTPAuthenticator httpAuthenticator = httpAuthenticatorType == null + ? null + : (HTTPAuthenticator) newInstance( + httpAuthenticatorType, + "h", + Settings.builder() + .put(opensearchSettings) + // .putProperties(ads.getAsStringMap(DotPath.of("http_authenticator.config")), + // DynamicConfiguration.checkKeyFunction()).build(), + .put( + Settings.builder() + .loadFromSource(ad.getValue().http_authenticator.configAsJson(), XContentType.JSON) + .build() + ) + .build() + + , + configPath + ); + + final AuthDomain _ad = new AuthDomain( + authenticationBackend, + httpAuthenticator, + ad.getValue().http_authenticator.challenge, + ad.getValue().order + ); if (httpEnabled && _ad.getHttpAuthenticator() != null) { restAuthDomains0.add(_ad); @@ -321,14 +371,19 @@ private void buildAAA() { destroyableComponents = Collections.unmodifiableList(destroyableComponents0); - if(originalDestroyableComponents != null) { + if (originalDestroyableComponents != null) { destroyDestroyables(originalDestroyableComponents); } originalDestroyableComponents = null; - createAuthFailureListeners(ipAuthFailureListeners0, - authBackendFailureListeners0, ipClientBlockRegistries0, authBackendClientBlockRegistries0, destroyableComponents0); + createAuthFailureListeners( + ipAuthFailureListeners0, + authBackendFailureListeners0, + ipClientBlockRegistries0, + authBackendClientBlockRegistries0, + destroyableComponents0 + ); ipAuthFailureListeners = Collections.unmodifiableList(ipAuthFailureListeners0); ipClientBlockRegistries = Collections.unmodifiableList(ipClientBlockRegistries0); @@ -351,8 +406,8 @@ private T newInstance(final String clazzOrShortcut, String type, final Setti String clazz = clazzOrShortcut; - if(authImplMap.containsKey(clazz+"_"+type)) { - clazz = authImplMap.get(clazz+"_"+type); + if (authImplMap.containsKey(clazz + "_" + type)) { + clazz = authImplMap.get(clazz + "_" + type); } return ReflectionHelper.instantiateAAA(clazz, settings, configPath); @@ -367,15 +422,20 @@ private String translateShortcutToClassName(final String clazzOrShortcut, final } } - private void createAuthFailureListeners(List ipAuthFailureListeners, - Multimap authBackendFailureListeners, List> ipClientBlockRegistries, - Multimap> authBackendUserClientBlockRegistries, List destroyableComponents0) { + private void createAuthFailureListeners( + List ipAuthFailureListeners, + Multimap authBackendFailureListeners, + List> ipClientBlockRegistries, + Multimap> authBackendUserClientBlockRegistries, + List destroyableComponents0 + ) { for (Entry entry : config.dynamic.auth_failure_listeners.getListeners().entrySet()) { Settings entrySettings = Settings.builder() - .put(opensearchSettings) - .put(Settings.builder().loadFromSource(entry.getValue().asJson(), XContentType.JSON).build()).build(); + .put(opensearchSettings) + .put(Settings.builder().loadFromSource(entry.getValue().asJson(), XContentType.JSON).build()) + .build(); String type = entry.getValue().type; String authenticationBackend = entry.getValue().authentication_backend; @@ -392,8 +452,13 @@ private void createAuthFailureListeners(List ipAuthFailureL ipClientBlockRegistries.add(clientBlockRegistry); } else { - log.error("Illegal ClientIdType for AuthFailureListener" + entry.getKey() + ": " - + ((ClientBlockRegistry) authFailureListener).getClientIdType() + "; must be InetAddress."); + log.error( + "Illegal ClientIdType for AuthFailureListener" + + entry.getKey() + + ": " + + ((ClientBlockRegistry) authFailureListener).getClientIdType() + + "; must be InetAddress." + ); } } @@ -410,8 +475,13 @@ private void createAuthFailureListeners(List ipAuthFailureL authBackendUserClientBlockRegistries.put(authenticationBackend, clientBlockRegistry); } else { - log.error("Illegal ClientIdType for AuthFailureListener" + entry.getKey() + ": " - + ((ClientBlockRegistry) authFailureListener).getClientIdType() + "; must be InetAddress."); + log.error( + "Illegal ClientIdType for AuthFailureListener" + + entry.getKey() + + ": " + + ((ClientBlockRegistry) authFailureListener).getClientIdType() + + "; must be InetAddress." + ); } } } diff --git a/src/main/java/org/opensearch/security/securityconf/EvaluatedDlsFlsConfig.java b/src/main/java/org/opensearch/security/securityconf/EvaluatedDlsFlsConfig.java index 8870cb3aad..aa22e8729f 100644 --- a/src/main/java/org/opensearch/security/securityconf/EvaluatedDlsFlsConfig.java +++ b/src/main/java/org/opensearch/security/securityconf/EvaluatedDlsFlsConfig.java @@ -21,14 +21,21 @@ import org.opensearch.security.support.WildcardMatcher; public class EvaluatedDlsFlsConfig { - public static EvaluatedDlsFlsConfig EMPTY = new EvaluatedDlsFlsConfig(Collections.emptyMap(), Collections.emptyMap(), Collections.emptyMap()); + public static EvaluatedDlsFlsConfig EMPTY = new EvaluatedDlsFlsConfig( + Collections.emptyMap(), + Collections.emptyMap(), + Collections.emptyMap() + ); private final Map> dlsQueriesByIndex; private final Map> flsByIndex; private final Map> fieldMaskingByIndex; - public EvaluatedDlsFlsConfig(Map> dlsQueriesByIndex, Map> flsByIndex, - Map> fieldMaskingByIndex) { + public EvaluatedDlsFlsConfig( + Map> dlsQueriesByIndex, + Map> flsByIndex, + Map> fieldMaskingByIndex + ) { this.dlsQueriesByIndex = Collections.unmodifiableMap(dlsQueriesByIndex); this.flsByIndex = Collections.unmodifiableMap(flsByIndex); this.fieldMaskingByIndex = Collections.unmodifiableMap(fieldMaskingByIndex); @@ -88,8 +95,11 @@ public EvaluatedDlsFlsConfig filter(Resolved indices) { } else { Set allIndices = indices.getAllIndices(); - return new EvaluatedDlsFlsConfig(filter(dlsQueriesByIndex, allIndices), filter(flsByIndex, allIndices), - filter(fieldMaskingByIndex, allIndices)); + return new EvaluatedDlsFlsConfig( + filter(dlsQueriesByIndex, allIndices), + filter(flsByIndex, allIndices), + filter(fieldMaskingByIndex, allIndices) + ); } } @@ -119,8 +129,13 @@ private Map> filter(Map> map, Set getBackenRoles(String user); + public abstract Map getAttributes(String user); + public abstract String getDescription(String user); + public abstract String getHash(String user); + public abstract List getSecurityRoles(String user); } diff --git a/src/main/java/org/opensearch/security/securityconf/Migration.java b/src/main/java/org/opensearch/security/securityconf/Migration.java index ec6a5525b9..4eeaba5c68 100644 --- a/src/main/java/org/opensearch/security/securityconf/Migration.java +++ b/src/main/java/org/opensearch/security/securityconf/Migration.java @@ -53,10 +53,12 @@ import org.opensearch.security.securityconf.impl.v7.RoleV7; import org.opensearch.security.securityconf.impl.v7.TenantV7; - public class Migration { - public static Tuple,SecurityDynamicConfiguration> migrateRoles(SecurityDynamicConfiguration r6cs, SecurityDynamicConfiguration rms6) throws MigrationException { + public static Tuple, SecurityDynamicConfiguration> migrateRoles( + SecurityDynamicConfiguration r6cs, + SecurityDynamicConfiguration rms6 + ) throws MigrationException { final SecurityDynamicConfiguration r7 = SecurityDynamicConfiguration.empty(); r7.setCType(r6cs.getCType()); @@ -72,11 +74,11 @@ public static Tuple,SecurityDynamicConfigur Set dedupTenants = new HashSet<>(); - for(final Entry r6e: r6cs.getCEntries().entrySet()) { - final String roleName = r6e.getKey(); + for (final Entry r6e : r6cs.getCEntries().entrySet()) { + final String roleName = r6e.getKey(); final RoleV6 r6 = r6e.getValue(); - if(r6 == null) { + if (r6 == null) { RoleV7 noPermRole = new RoleV7(); noPermRole.setDescription("Migrated from v6, was empty"); r7.putCEntry(roleName, noPermRole); @@ -85,18 +87,18 @@ public static Tuple,SecurityDynamicConfigur r7.putCEntry(roleName, new RoleV7(r6)); - for(Entry tenant: r6.getTenants().entrySet()) { + for (Entry tenant : r6.getTenants().entrySet()) { dedupTenants.add(tenant.getKey()); } } - if(rms6 != null) { - for(final Entry r6m: rms6.getCEntries().entrySet()) { - final String roleName = r6m.getKey(); - //final RoleMappingsV6 r6 = r6m.getValue(); + if (rms6 != null) { + for (final Entry r6m : rms6.getCEntries().entrySet()) { + final String roleName = r6m.getKey(); + // final RoleMappingsV6 r6 = r6m.getValue(); - if(!r7.exists(roleName)) { - //rolemapping but role does not exists + if (!r7.exists(roleName)) { + // rolemapping but role does not exists RoleV7 noPermRole = new RoleV7(); noPermRole.setDescription("Migrated from v6, was in rolemappings but no role existed"); r7.putCEntry(roleName, noPermRole); @@ -105,7 +107,7 @@ public static Tuple,SecurityDynamicConfigur } } - for(String tenantName: dedupTenants) { + for (String tenantName : dedupTenants) { TenantV7 entry = new TenantV7(); entry.setDescription("Migrated from v6"); t7.putCEntry(tenantName, entry); @@ -115,22 +117,25 @@ public static Tuple,SecurityDynamicConfigur } - public static SecurityDynamicConfiguration migrateConfig(SecurityDynamicConfiguration r6cs) throws MigrationException { + public static SecurityDynamicConfiguration migrateConfig(SecurityDynamicConfiguration r6cs) + throws MigrationException { final SecurityDynamicConfiguration c7 = SecurityDynamicConfiguration.empty(); c7.setCType(r6cs.getCType()); c7.set_meta(new Meta()); c7.get_meta().setConfig_version(2); c7.get_meta().setType("config"); - if(r6cs.getCEntries().size() != 1) { - throw new MigrationException("Unable to migrate config because expected size was 1 but actual size is "+r6cs.getCEntries().size()); + if (r6cs.getCEntries().size() != 1) { + throw new MigrationException( + "Unable to migrate config because expected size was 1 but actual size is " + r6cs.getCEntries().size() + ); } - if(r6cs.getCEntries().get("opendistro_security") == null) { + if (r6cs.getCEntries().get("opendistro_security") == null) { throw new MigrationException("Unable to migrate config because 'opendistro_security' key not found"); } - for(final Entry r6c: r6cs.getCEntries().entrySet()) { + for (final Entry r6c : r6cs.getCEntries().entrySet()) { c7.putCEntry("config", new ConfigV7(r6c.getValue())); } return c7; @@ -143,54 +148,60 @@ public static SecurityDynamicConfiguration migrateNodesDn(SecurityDynam migrated.get_meta().setConfig_version(2); migrated.get_meta().setType("nodesdn"); - for(final Entry entry: nodesDn.getCEntries().entrySet()) { + for (final Entry entry : nodesDn.getCEntries().entrySet()) { migrated.putCEntry(entry.getKey(), new NodesDn(entry.getValue())); } return migrated; } - public static SecurityDynamicConfiguration migrateWhitelistingSetting(SecurityDynamicConfiguration whitelistingSetting) { + public static SecurityDynamicConfiguration migrateWhitelistingSetting( + SecurityDynamicConfiguration whitelistingSetting + ) { final SecurityDynamicConfiguration migrated = SecurityDynamicConfiguration.empty(); migrated.setCType(whitelistingSetting.getCType()); migrated.set_meta(new Meta()); migrated.get_meta().setConfig_version(2); migrated.get_meta().setType("whitelist"); - for(final Entry entry: whitelistingSetting.getCEntries().entrySet()) { + for (final Entry entry : whitelistingSetting.getCEntries().entrySet()) { migrated.putCEntry(entry.getKey(), new WhitelistingSettings(entry.getValue())); } return migrated; } - public static SecurityDynamicConfiguration migrateAllowlistingSetting(SecurityDynamicConfiguration allowlistingSetting) { + public static SecurityDynamicConfiguration migrateAllowlistingSetting( + SecurityDynamicConfiguration allowlistingSetting + ) { final SecurityDynamicConfiguration migrated = SecurityDynamicConfiguration.empty(); migrated.setCType(allowlistingSetting.getCType()); migrated.set_meta(new Meta()); migrated.get_meta().setConfig_version(2); migrated.get_meta().setType("whitelist"); - for(final Entry entry: allowlistingSetting.getCEntries().entrySet()) { + for (final Entry entry : allowlistingSetting.getCEntries().entrySet()) { migrated.putCEntry(entry.getKey(), new AllowlistingSettings(entry.getValue())); } return migrated; } - public static SecurityDynamicConfiguration migrateInternalUsers(SecurityDynamicConfiguration r6is) throws MigrationException { + public static SecurityDynamicConfiguration migrateInternalUsers(SecurityDynamicConfiguration r6is) + throws MigrationException { final SecurityDynamicConfiguration i7 = SecurityDynamicConfiguration.empty(); i7.setCType(r6is.getCType()); i7.set_meta(new Meta()); i7.get_meta().setConfig_version(2); i7.get_meta().setType("internalusers"); - for(final Entry r6i: r6is.getCEntries().entrySet()) { - final String username = !Strings.isNullOrEmpty(r6i.getValue().getUsername())?r6i.getValue().getUsername():r6i.getKey(); + for (final Entry r6i : r6is.getCEntries().entrySet()) { + final String username = !Strings.isNullOrEmpty(r6i.getValue().getUsername()) ? r6i.getValue().getUsername() : r6i.getKey(); i7.putCEntry(username, new InternalUserV7(r6i.getValue())); } return i7; } - public static SecurityDynamicConfiguration migrateActionGroups(SecurityDynamicConfiguration r6as) throws MigrationException { + public static SecurityDynamicConfiguration migrateActionGroups(SecurityDynamicConfiguration r6as) + throws MigrationException { final SecurityDynamicConfiguration a7 = SecurityDynamicConfiguration.empty(); a7.setCType(r6as.getCType()); @@ -198,27 +209,28 @@ public static SecurityDynamicConfiguration migrateActionGroups( a7.get_meta().setConfig_version(2); a7.get_meta().setType("actiongroups"); - if(r6as.getImplementingClass().isAssignableFrom(List.class)) { - for(final Entry r6a: r6as.getCEntries().entrySet()) { + if (r6as.getImplementingClass().isAssignableFrom(List.class)) { + for (final Entry r6a : r6as.getCEntries().entrySet()) { a7.putCEntry(r6a.getKey(), new ActionGroupsV7(r6a.getKey(), (List) r6a.getValue())); } } else { - for(final Entry r6a: r6as.getCEntries().entrySet()) { - a7.putCEntry(r6a.getKey(), new ActionGroupsV7(r6a.getKey(), (ActionGroupsV6)r6a.getValue())); + for (final Entry r6a : r6as.getCEntries().entrySet()) { + a7.putCEntry(r6a.getKey(), new ActionGroupsV7(r6a.getKey(), (ActionGroupsV6) r6a.getValue())); } } return a7; } - public static SecurityDynamicConfiguration migrateRoleMappings(SecurityDynamicConfiguration r6rms) throws MigrationException { + public static SecurityDynamicConfiguration migrateRoleMappings(SecurityDynamicConfiguration r6rms) + throws MigrationException { final SecurityDynamicConfiguration rms7 = SecurityDynamicConfiguration.empty(); rms7.setCType(r6rms.getCType()); rms7.set_meta(new Meta()); rms7.get_meta().setConfig_version(2); rms7.get_meta().setType("rolesmapping"); - for(final Entry r6m: r6rms.getCEntries().entrySet()) { + for (final Entry r6m : r6rms.getCEntries().entrySet()) { rms7.putCEntry(r6m.getKey(), new RoleMappingsV7(r6m.getValue())); } @@ -232,7 +244,7 @@ public static SecurityDynamicConfiguration migrateAudit(SecurityDyn migrated.get_meta().setConfig_version(2); migrated.get_meta().setType("audit"); - for(final Entry entry: audit.getCEntries().entrySet()) { + for (final Entry entry : audit.getCEntries().entrySet()) { migrated.putCEntry(entry.getKey(), entry.getValue()); } return migrated; diff --git a/src/main/java/org/opensearch/security/securityconf/RoleMappings.java b/src/main/java/org/opensearch/security/securityconf/RoleMappings.java index 01d686733e..ff4153898b 100644 --- a/src/main/java/org/opensearch/security/securityconf/RoleMappings.java +++ b/src/main/java/org/opensearch/security/securityconf/RoleMappings.java @@ -16,8 +16,8 @@ public class RoleMappings { - private List hosts= Collections.emptyList(); - private List users= Collections.emptyList(); + private List hosts = Collections.emptyList(); + private List users = Collections.emptyList(); public void setHosts(List hosts) { this.hosts = hosts; diff --git a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java index de7afbc27b..c52a3c1bad 100644 --- a/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java +++ b/src/main/java/org/opensearch/security/securityconf/SecurityRoles.java @@ -43,15 +43,45 @@ public interface SecurityRoles { Set getRoleNames(); - Set reduce(Resolved requestedResolved, User user, String[] strings, IndexNameExpressionResolver resolver, ClusterService clusterService); + Set reduce( + Resolved requestedResolved, + User user, + String[] strings, + IndexNameExpressionResolver resolver, + ClusterService clusterService + ); - boolean impliesTypePermGlobal(Resolved requestedResolved, User user, String[] allIndexPermsRequiredA, IndexNameExpressionResolver resolver, ClusterService clusterService); + boolean impliesTypePermGlobal( + Resolved requestedResolved, + User user, + String[] allIndexPermsRequiredA, + IndexNameExpressionResolver resolver, + ClusterService clusterService + ); - boolean get(Resolved requestedResolved, User user, String[] allIndexPermsRequiredA, IndexNameExpressionResolver resolver, ClusterService clusterService); + boolean get( + Resolved requestedResolved, + User user, + String[] allIndexPermsRequiredA, + IndexNameExpressionResolver resolver, + ClusterService clusterService + ); - EvaluatedDlsFlsConfig getDlsFls(User user, boolean dfmEmptyOverwritesAll, IndexNameExpressionResolver resolver, ClusterService clusterService, NamedXContentRegistry namedXContentRegistry); + EvaluatedDlsFlsConfig getDlsFls( + User user, + boolean dfmEmptyOverwritesAll, + IndexNameExpressionResolver resolver, + ClusterService clusterService, + NamedXContentRegistry namedXContentRegistry + ); - Set getAllPermittedIndicesForDashboards(Resolved resolved, User user, String[] actions, IndexNameExpressionResolver resolver, ClusterService cs); + Set getAllPermittedIndicesForDashboards( + Resolved resolved, + User user, + String[] actions, + IndexNameExpressionResolver resolver, + ClusterService cs + ); SecurityRoles filter(Set roles); diff --git a/src/main/java/org/opensearch/security/securityconf/impl/AllowlistingSettings.java b/src/main/java/org/opensearch/security/securityconf/impl/AllowlistingSettings.java index 131c43b50b..e2c86009d1 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/AllowlistingSettings.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/AllowlistingSettings.java @@ -48,7 +48,7 @@ public void setEnabled(boolean enabled) { } public Map> getRequests() { - return this.requests == null ? Collections.emptyMap(): this.requests; + return this.requests == null ? Collections.emptyMap() : this.requests; } public void setRequests(Map> requests) { @@ -60,7 +60,6 @@ public String toString() { return "AllowlistingSetting [enabled=" + enabled + ", requests=" + requests + ']'; } - /** * Helper function to check if a rest request is allowlisted, by checking if the path is allowlisted, * and then if the Http method is allowlisted. @@ -76,26 +75,26 @@ public String toString() { * GET /_cluster/settings - OK * GET /_cluster/settings/ - OK */ - private boolean requestIsAllowlisted(RestRequest request){ + private boolean requestIsAllowlisted(RestRequest request) { - //ALSO ALLOWS REQUEST TO HAVE TRAILING '/' - //pathWithoutTrailingSlash stores the endpoint path without extra '/'. eg: /_cat/nodes - //pathWithTrailingSlash stores the endpoint path with extra '/'. eg: /_cat/nodes/ + // ALSO ALLOWS REQUEST TO HAVE TRAILING '/' + // pathWithoutTrailingSlash stores the endpoint path without extra '/'. eg: /_cat/nodes + // pathWithTrailingSlash stores the endpoint path with extra '/'. eg: /_cat/nodes/ String path = request.path(); String pathWithoutTrailingSlash; String pathWithTrailingSlash; - //first obtain pathWithoutTrailingSlash, then add a '/' to it to get pathWithTrailingSlash + // first obtain pathWithoutTrailingSlash, then add a '/' to it to get pathWithTrailingSlash pathWithoutTrailingSlash = path.endsWith("/") ? path.substring(0, path.length() - 1) : path; pathWithTrailingSlash = pathWithoutTrailingSlash + '/'; - //check if pathWithoutTrailingSlash is allowlisted - if(requests.containsKey(pathWithoutTrailingSlash) && requests.get(pathWithoutTrailingSlash).contains(HttpRequestMethods.valueOf(request.method().toString()))) - return true; + // check if pathWithoutTrailingSlash is allowlisted + if (requests.containsKey(pathWithoutTrailingSlash) + && requests.get(pathWithoutTrailingSlash).contains(HttpRequestMethods.valueOf(request.method().toString()))) return true; - //check if pathWithTrailingSlash is allowlisted - if(requests.containsKey(pathWithTrailingSlash) && requests.get(pathWithTrailingSlash).contains(HttpRequestMethods.valueOf(request.method().toString()))) - return true; + // check if pathWithTrailingSlash is allowlisted + if (requests.containsKey(pathWithTrailingSlash) + && requests.get(pathWithTrailingSlash).contains(HttpRequestMethods.valueOf(request.method().toString()))) return true; return false; } @@ -108,15 +107,19 @@ private boolean requestIsAllowlisted(RestRequest request){ * then all PUT /_opendistro/_security/api/rolesmapping/{resource_name} work. * Currently, each resource_name has to be allowlisted separately */ - public boolean checkRequestIsAllowed(RestRequest request, RestChannel channel, - NodeClient client) throws IOException { + public boolean checkRequestIsAllowed(RestRequest request, RestChannel channel, NodeClient client) throws IOException { // if allowlisting is enabled but the request is not allowlisted, then return false, otherwise true. - if (this.enabled && !requestIsAllowlisted(request)){ - channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, channel.newErrorBuilder().startObject() - .field("error", request.method() + " " + request.path() + " API not allowlisted") - .field("status", RestStatus.FORBIDDEN) - .endObject() - )); + if (this.enabled && !requestIsAllowlisted(request)) { + channel.sendResponse( + new BytesRestResponse( + RestStatus.FORBIDDEN, + channel.newErrorBuilder() + .startObject() + .field("error", request.method() + " " + request.path() + " API not allowlisted") + .field("status", RestStatus.FORBIDDEN) + .endObject() + ) + ); return false; } return true; diff --git a/src/main/java/org/opensearch/security/securityconf/impl/CType.java b/src/main/java/org/opensearch/security/securityconf/impl/CType.java index 5d9f1f307b..4e5e2de496 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/CType.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/CType.java @@ -50,10 +50,8 @@ public enum CType { - INTERNALUSERS(toMap(1, InternalUserV6.class, 2, - InternalUserV7.class)), - ACTIONGROUPS(toMap(0, List.class, 1, ActionGroupsV6.class, 2, - ActionGroupsV7.class)), + INTERNALUSERS(toMap(1, InternalUserV6.class, 2, InternalUserV7.class)), + ACTIONGROUPS(toMap(0, List.class, 1, ActionGroupsV6.class, 2, ActionGroupsV7.class)), CONFIG(toMap(1, ConfigV6.class, 2, ConfigV7.class)), ROLES(toMap(1, RoleV6.class, 2, RoleV7.class)), ROLESMAPPING(toMap(1, RoleMappingsV6.class, 2, RoleMappingsV7.class)), diff --git a/src/main/java/org/opensearch/security/securityconf/impl/Meta.java b/src/main/java/org/opensearch/security/securityconf/impl/Meta.java index 1e9060efa1..2fa4ced06a 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/Meta.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/Meta.java @@ -31,7 +31,6 @@ public class Meta { - private String type; private int config_version; @@ -45,9 +44,11 @@ public void setType(String type) { this.type = type; cType = CType.fromString(type); } + public int getConfig_version() { return config_version; } + public void setConfig_version(int config_version) { this.config_version = config_version; } @@ -62,5 +63,4 @@ public String toString() { return "Meta [type=" + type + ", config_version=" + config_version + ", cType=" + cType + "]"; } - } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java index c282f439e8..e86eed779c 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/SecurityDynamicConfiguration.java @@ -40,9 +40,6 @@ import com.fasterxml.jackson.databind.JsonNode; import org.opensearch.ExceptionsHelper; -import org.opensearch.common.bytes.BytesReference; -import org.opensearch.common.xcontent.XContentHelper; -import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.xcontent.ToXContent; import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.security.DefaultObjectMapper; @@ -53,12 +50,13 @@ public class SecurityDynamicConfiguration implements ToXContent { - private static final TypeReference> typeRefMSO = new TypeReference>() {}; + private static final TypeReference> typeRefMSO = new TypeReference>() { + }; @JsonIgnore private final Map centries = new HashMap<>(); - private long seqNo= -1; - private long primaryTerm= -1; + private long seqNo = -1; + private long primaryTerm = -1; private CType ctype; private int version = -1; @@ -66,21 +64,36 @@ public static SecurityDynamicConfiguration empty() { return new SecurityDynamicConfiguration(); } - public static SecurityDynamicConfiguration fromJson(String json, CType ctype, int version, long seqNo, long primaryTerm) throws IOException { + public static SecurityDynamicConfiguration fromJson(String json, CType ctype, int version, long seqNo, long primaryTerm) + throws IOException { return fromJson(json, ctype, version, seqNo, primaryTerm, false); } - public static SecurityDynamicConfiguration fromJson(String json, CType ctype, int version, long seqNo, long primaryTerm, boolean acceptInvalid) throws IOException { + public static SecurityDynamicConfiguration fromJson( + String json, + CType ctype, + int version, + long seqNo, + long primaryTerm, + boolean acceptInvalid + ) throws IOException { SecurityDynamicConfiguration sdc = null; - if(ctype != null) { + if (ctype != null) { final Class implementationClass = ctype.getImplementationClass().get(version); - if(implementationClass == null) { - throw new IllegalArgumentException("No implementation class found for "+ctype+" and config version "+version); + if (implementationClass == null) { + throw new IllegalArgumentException("No implementation class found for " + ctype + " and config version " + version); } - if(acceptInvalid && version < 2) { - sdc = NonValidatingObjectMapper.readValue(json, NonValidatingObjectMapper.getTypeFactory().constructParametricType(SecurityDynamicConfiguration.class, implementationClass)); + if (acceptInvalid && version < 2) { + sdc = NonValidatingObjectMapper.readValue( + json, + NonValidatingObjectMapper.getTypeFactory() + .constructParametricType(SecurityDynamicConfiguration.class, implementationClass) + ); } else { - sdc = DefaultObjectMapper.readValue(json, DefaultObjectMapper.getTypeFactory().constructParametricType(SecurityDynamicConfiguration.class, implementationClass)); + sdc = DefaultObjectMapper.readValue( + json, + DefaultObjectMapper.getTypeFactory().constructParametricType(SecurityDynamicConfiguration.class, implementationClass) + ); } validate(sdc, version, ctype); @@ -97,29 +110,32 @@ public static SecurityDynamicConfiguration fromJson(String json, CType ct } public static void validate(SecurityDynamicConfiguration sdc, int version, CType ctype) throws IOException { - if(version < 2 && sdc.get_meta() != null) { - throw new IOException("A version of "+version+" can not have a _meta key for "+ctype); + if (version < 2 && sdc.get_meta() != null) { + throw new IOException("A version of " + version + " can not have a _meta key for " + ctype); } - if(version >= 2 && sdc.get_meta() == null) { - throw new IOException("A version of "+version+" must have a _meta key for "+ctype); + if (version >= 2 && sdc.get_meta() == null) { + throw new IOException("A version of " + version + " must have a _meta key for " + ctype); } - if(version < 2 && ctype == CType.CONFIG && (sdc.getCEntries().size() != 1 || !sdc.getCEntries().keySet().contains("opendistro_security"))) { - throw new IOException("A version of "+version+" must have a single toplevel key named 'opendistro_security' for "+ctype); + if (version < 2 + && ctype == CType.CONFIG + && (sdc.getCEntries().size() != 1 || !sdc.getCEntries().keySet().contains("opendistro_security"))) { + throw new IOException("A version of " + version + " must have a single toplevel key named 'opendistro_security' for " + ctype); } - if(version >= 2 && ctype == CType.CONFIG && (sdc.getCEntries().size() != 1 || !sdc.getCEntries().keySet().contains("config"))) { - throw new IOException("A version of "+version+" must have a single toplevel key named 'config' for "+ctype); + if (version >= 2 && ctype == CType.CONFIG && (sdc.getCEntries().size() != 1 || !sdc.getCEntries().keySet().contains("config"))) { + throw new IOException("A version of " + version + " must have a single toplevel key named 'config' for " + ctype); } } - public static SecurityDynamicConfiguration fromNode(JsonNode json, CType ctype, int version, long seqNo, long primaryTerm) throws IOException { + public static SecurityDynamicConfiguration fromNode(JsonNode json, CType ctype, int version, long seqNo, long primaryTerm) + throws IOException { return fromJson(DefaultObjectMapper.writeValueAsString(json, false), ctype, version, seqNo, primaryTerm); } - //for Jackson + // for Jackson private SecurityDynamicConfiguration() { super(); } @@ -134,7 +150,6 @@ public void set_meta(Meta _meta) { this._meta = _meta; } - @JsonAnySetter void setCEntries(String key, T value) { putCEntry(key, value); @@ -147,8 +162,8 @@ public Map getCEntries() { @JsonIgnore public void removeHidden() { - for(Entry entry: new HashMap(centries).entrySet()) { - if(entry.getValue() instanceof Hideable && ((Hideable) entry.getValue()).isHidden()) { + for (Entry entry : new HashMap(centries).entrySet()) { + if (entry.getValue() instanceof Hideable && ((Hideable) entry.getValue()).isHidden()) { centries.remove(entry.getKey()); } } @@ -156,8 +171,8 @@ public void removeHidden() { @JsonIgnore public void removeStatic() { - for(Entry entry: new HashMap(centries).entrySet()) { - if(entry.getValue() instanceof StaticDefinable && ((StaticDefinable) entry.getValue()).isStatic()) { + for (Entry entry : new HashMap(centries).entrySet()) { + if (entry.getValue() instanceof StaticDefinable && ((StaticDefinable) entry.getValue()).isStatic()) { centries.remove(entry.getKey()); } } @@ -165,14 +180,13 @@ public void removeStatic() { @JsonIgnore public void clearHashes() { - for(Entry entry: centries.entrySet()) { - if(entry.getValue() instanceof Hashed) { - ((Hashed) entry.getValue()).clearHash(); + for (Entry entry : centries.entrySet()) { + if (entry.getValue() instanceof Hashed) { + ((Hashed) entry.getValue()).clearHash(); } } } - public void removeOthers(String key) { T tmp = this.centries.get(key); this.centries.clear(); @@ -199,15 +213,21 @@ public boolean exists(String key) { return centries.containsKey(key); } - @JsonIgnore - public BytesReference toBytesReference() throws IOException { - return XContentHelper.toXContent(this, XContentType.JSON, false); - } - @Override public String toString() { - return "SecurityDynamicConfiguration [seqNo=" + seqNo + ", primaryTerm=" + primaryTerm + ", ctype=" + ctype + ", version=" + version + ", centries=" - + centries + ", getImplementingClass()=" + getImplementingClass() + "]"; + return "SecurityDynamicConfiguration [seqNo=" + + seqNo + + ", primaryTerm=" + + primaryTerm + + ", ctype=" + + ctype + + ", version=" + + version + + ", centries=" + + centries + + ", getImplementingClass()=" + + getImplementingClass() + + "]"; } @Override @@ -250,7 +270,7 @@ public int getVersion() { @JsonIgnore public Class getImplementingClass() { - return ctype==null?null:ctype.getImplementationClass().get(getVersion()); + return ctype == null ? null : ctype.getImplementationClass().get(getVersion()); } @JsonIgnore @@ -264,21 +284,21 @@ public SecurityDynamicConfiguration deepClone() { @JsonIgnore public void remove(String key) { - centries.remove(key); + centries.remove(key); } @SuppressWarnings({ "rawtypes", "unchecked" }) public boolean add(SecurityDynamicConfiguration other) { - if(other.ctype == null || !other.ctype.equals(this.ctype)) { + if (other.ctype == null || !other.ctype.equals(this.ctype)) { return false; } - if(other.getImplementingClass() == null || !other.getImplementingClass().equals(this.getImplementingClass())) { + if (other.getImplementingClass() == null || !other.getImplementingClass().equals(this.getImplementingClass())) { return false; } - if(other.version != this.version) { + if (other.version != this.version) { return false; } @@ -292,9 +312,21 @@ public boolean containsAny(SecurityDynamicConfiguration other) { return !Collections.disjoint(this.centries.keySet(), other.centries.keySet()); } - public boolean isHidden(String resourceName){ + public boolean isHidden(String resourceName) { + final Object o = centries.get(resourceName); + return o instanceof Hideable && ((Hideable) o).isHidden(); + } + + @JsonIgnore + public boolean isStatic(final String resourceName) { + final Object o = centries.get(resourceName); + return o instanceof StaticDefinable && ((StaticDefinable) o).isStatic(); + } + + @JsonIgnore + public boolean isReserved(final String resourceName) { final Object o = centries.get(resourceName); - return o != null && o instanceof Hideable && ((Hideable) o).isHidden(); + return o instanceof Hideable && ((Hideable) o).isReserved(); } } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java b/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java index b6dfbaae4e..57405b24fe 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/WhitelistingSettings.java @@ -48,7 +48,7 @@ public void setEnabled(boolean enabled) { } public Map> getRequests() { - return this.requests == null ? Collections.emptyMap(): this.requests; + return this.requests == null ? Collections.emptyMap() : this.requests; } public void setRequests(Map> requests) { @@ -60,7 +60,6 @@ public String toString() { return "WhitelistingSetting [enabled=" + enabled + ", requests=" + requests + ']'; } - /** * Helper function to check if a rest request is whitelisted, by checking if the path is whitelisted, * and then if the Http method is whitelisted. @@ -76,26 +75,26 @@ public String toString() { * GET /_cluster/settings - OK * GET /_cluster/settings/ - OK */ - private boolean requestIsWhitelisted(RestRequest request){ + private boolean requestIsWhitelisted(RestRequest request) { - //ALSO ALLOWS REQUEST TO HAVE TRAILING '/' - //pathWithoutTrailingSlash stores the endpoint path without extra '/'. eg: /_cat/nodes - //pathWithTrailingSlash stores the endpoint path with extra '/'. eg: /_cat/nodes/ + // ALSO ALLOWS REQUEST TO HAVE TRAILING '/' + // pathWithoutTrailingSlash stores the endpoint path without extra '/'. eg: /_cat/nodes + // pathWithTrailingSlash stores the endpoint path with extra '/'. eg: /_cat/nodes/ String path = request.path(); String pathWithoutTrailingSlash; String pathWithTrailingSlash; - //first obtain pathWithoutTrailingSlash, then add a '/' to it to get pathWithTrailingSlash + // first obtain pathWithoutTrailingSlash, then add a '/' to it to get pathWithTrailingSlash pathWithoutTrailingSlash = path.endsWith("/") ? path.substring(0, path.length() - 1) : path; pathWithTrailingSlash = pathWithoutTrailingSlash + '/'; - //check if pathWithoutTrailingSlash is whitelisted - if(requests.containsKey(pathWithoutTrailingSlash) && requests.get(pathWithoutTrailingSlash).contains(HttpRequestMethods.valueOf(request.method().toString()))) - return true; + // check if pathWithoutTrailingSlash is whitelisted + if (requests.containsKey(pathWithoutTrailingSlash) + && requests.get(pathWithoutTrailingSlash).contains(HttpRequestMethods.valueOf(request.method().toString()))) return true; - //check if pathWithTrailingSlash is whitelisted - if(requests.containsKey(pathWithTrailingSlash) && requests.get(pathWithTrailingSlash).contains(HttpRequestMethods.valueOf(request.method().toString()))) - return true; + // check if pathWithTrailingSlash is whitelisted + if (requests.containsKey(pathWithTrailingSlash) + && requests.get(pathWithTrailingSlash).contains(HttpRequestMethods.valueOf(request.method().toString()))) return true; return false; } @@ -109,15 +108,19 @@ private boolean requestIsWhitelisted(RestRequest request){ * Currently, each resource_name has to be whitelisted separately */ @Override - public boolean checkRequestIsAllowed(RestRequest request, RestChannel channel, - NodeClient client) throws IOException { + public boolean checkRequestIsAllowed(RestRequest request, RestChannel channel, NodeClient client) throws IOException { // if whitelisting is enabled but the request is not whitelisted, then return false, otherwise true. - if (this.enabled && !requestIsWhitelisted(request)){ - channel.sendResponse(new BytesRestResponse(RestStatus.FORBIDDEN, channel.newErrorBuilder().startObject() - .field("error", request.method() + " " + request.path() + " API not whitelisted") - .field("status", RestStatus.FORBIDDEN) - .endObject() - )); + if (this.enabled && !requestIsWhitelisted(request)) { + channel.sendResponse( + new BytesRestResponse( + RestStatus.FORBIDDEN, + channel.newErrorBuilder() + .startObject() + .field("error", request.method() + " " + request.path() + " API not whitelisted") + .field("status", RestStatus.FORBIDDEN) + .endObject() + ) + ); return false; } return true; diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v6/ActionGroupsV6.java b/src/main/java/org/opensearch/security/securityconf/impl/v6/ActionGroupsV6.java index 99bef31505..45fced168c 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v6/ActionGroupsV6.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v6/ActionGroupsV6.java @@ -35,7 +35,6 @@ public class ActionGroupsV6 implements Hideable { - private boolean readonly; private boolean hidden; private List permissions = Collections.emptyList(); @@ -48,28 +47,34 @@ public ActionGroupsV6() { public boolean isReserved() { return readonly; } + public boolean isReadonly() { return readonly; } + public void setReadonly(boolean readonly) { this.readonly = readonly; } + public boolean isHidden() { return hidden; } + public void setHidden(boolean hidden) { this.hidden = hidden; } + public List getPermissions() { return permissions; } + public void setPermissions(List permissions) { this.permissions = permissions; } + @Override public String toString() { return "ActionGroups [readonly=" + readonly + ", hidden=" + hidden + ", permissions=" + permissions + "]"; } - } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java index 3692bb59ca..01375b1f97 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v6/ConfigV6.java @@ -25,7 +25,6 @@ * GitHub history for details. */ - package org.opensearch.security.securityconf.impl.v6; import java.util.Collections; @@ -47,8 +46,6 @@ public class ConfigV6 { public Dynamic dynamic; - - @Override public String toString() { return "Config [dynamic=" + dynamic + "]"; @@ -56,7 +53,6 @@ public String toString() { public static class Dynamic { - public String filtered_alias_mode = "warn"; public boolean disable_rest_auth; public boolean disable_intertransport_auth; @@ -78,8 +74,17 @@ public static class Dynamic { @Override public String toString() { - return "Dynamic [filtered_alias_mode=" + filtered_alias_mode + ", kibana=" + kibana + ", http=" + http + ", authc=" + authc + ", authz=" - + authz + ", on_behalf_of=" + on_behalf_of + "]"; + return "Dynamic [filtered_alias_mode=" + + filtered_alias_mode + + ", kibana=" + + kibana + + ", http=" + + http + + ", authc=" + + authc + + ", authz=" + + authz + + "]"; } } @@ -95,25 +100,33 @@ public static class Kibana { public String opendistro_role = null; public String index = ".kibana"; public boolean do_not_fail_on_forbidden; + @Override public String toString() { - return "Kibana [multitenancy_enabled=" + multitenancy_enabled + ", server_username=" + server_username + ", opendistro_role=" + opendistro_role - + ", index=" + index + ", do_not_fail_on_forbidden=" + do_not_fail_on_forbidden + "]"; + return "Kibana [multitenancy_enabled=" + + multitenancy_enabled + + ", server_username=" + + server_username + + ", opendistro_role=" + + opendistro_role + + ", index=" + + index + + ", do_not_fail_on_forbidden=" + + do_not_fail_on_forbidden + + "]"; } - - } public static class Http { public boolean anonymous_auth_enabled = false; public Xff xff = new Xff(); + @Override public String toString() { return "Http [anonymous_auth_enabled=" + anonymous_auth_enabled + ", xff=" + xff + "]"; } - } public static class AuthFailureListeners { @@ -130,7 +143,6 @@ public Map getListeners() { return listeners; } - } public static class AuthFailureListener { @@ -160,23 +172,33 @@ public static class Xff { @JsonInclude(JsonInclude.Include.NON_NULL) public boolean enabled = true; public String 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}").toString(); - public String remoteIpHeader="X-Forwarded-For"; - public String proxiesHeader="X-Forwarded-By"; + "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}" + ).toString(); + public String remoteIpHeader = "X-Forwarded-For"; + public String proxiesHeader = "X-Forwarded-By"; public String trustedProxies; + @Override public String toString() { - return "Xff [enabled=" + enabled + ", internalProxies=" + internalProxies + ", remoteIpHeader=" + remoteIpHeader + ", proxiesHeader=" - + proxiesHeader + ", trustedProxies=" + trustedProxies + "]"; + return "Xff [enabled=" + + enabled + + ", internalProxies=" + + internalProxies + + ", remoteIpHeader=" + + remoteIpHeader + + ", proxiesHeader=" + + proxiesHeader + + ", trustedProxies=" + + trustedProxies + + "]"; } - } public static class Authc { @@ -199,26 +221,36 @@ public String toString() { return "Authc [domains=" + domains + "]"; } - } public static class AuthcDomain { @JsonInclude(JsonInclude.Include.NON_NULL) - public boolean http_enabled= true; + public boolean http_enabled = true; @JsonInclude(JsonInclude.Include.NON_NULL) - public boolean transport_enabled= true; + public boolean transport_enabled = true; @JsonInclude(JsonInclude.Include.NON_NULL) - public boolean enabled= true; + public boolean enabled = true; public int order = 0; public HttpAuthenticator http_authenticator = new HttpAuthenticator(); public AuthcBackend authentication_backend = new AuthcBackend(); + @Override public String toString() { - return "AuthcDomain [http_enabled=" + http_enabled + ", transport_enabled=" + transport_enabled + ", enabled=" + enabled + ", order=" - + order + ", http_authenticator=" + http_authenticator + ", authentication_backend=" + authentication_backend + "]"; + return "AuthcDomain [http_enabled=" + + http_enabled + + ", transport_enabled=" + + transport_enabled + + ", enabled=" + + enabled + + ", order=" + + order + + ", http_authenticator=" + + http_authenticator + + ", authentication_backend=" + + authentication_backend + + "]"; } - } public static class HttpAuthenticator { @@ -241,7 +273,6 @@ public String toString() { return "HttpAuthenticator [challenge=" + challenge + ", type=" + type + ", config=" + config + "]"; } - } public static class AuthzBackend { @@ -262,7 +293,6 @@ public String toString() { return "AuthzBackend [type=" + type + ", config=" + config + "]"; } - } public static class AuthcBackend { @@ -283,7 +313,6 @@ public String toString() { return "AuthcBackend [type=" + type + ", config=" + config + "]"; } - } public static class Authz { @@ -305,7 +334,6 @@ public String toString() { return "Authz [domains=" + domains + "]"; } - } public static class AuthzDomain { @@ -316,12 +344,20 @@ public static class AuthzDomain { @JsonInclude(JsonInclude.Include.NON_NULL) public boolean enabled = true; public AuthzBackend authorization_backend = new AuthzBackend(); + @Override public String toString() { - return "AuthzDomain [http_enabled=" + http_enabled + ", transport_enabled=" + transport_enabled + ", enabled=" + enabled + ", authorization_backend=" + authorization_backend + "]"; + return "AuthzDomain [http_enabled=" + + http_enabled + + ", transport_enabled=" + + transport_enabled + + ", enabled=" + + enabled + + ", authorization_backend=" + + authorization_backend + + "]"; } - } public static class OnBehalfOf { @@ -348,7 +384,7 @@ public void setEncryptionKey(String encryptionKey) { @Override public String toString() { - return "OnBehalfOf [signing_key=" + signingKey + ", encryption_key=" + encryptionKey +"]"; + return "OnBehalfOf [signing_key=" + signingKey + ", encryption_key=" + encryptionKey + "]"; } } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v6/InternalUserV6.java b/src/main/java/org/opensearch/security/securityconf/impl/v6/InternalUserV6.java index f650101b64..0db727ad99 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v6/InternalUserV6.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v6/InternalUserV6.java @@ -38,91 +38,111 @@ public class InternalUserV6 implements Hideable, Hashed { - private String hash; - private boolean readonly; - private boolean hidden; - private List roles = Collections.emptyList(); - private Map attributes = Collections.emptyMap(); - private String username; - - - - public InternalUserV6(String hash, boolean readonly, boolean hidden, List roles, Map attributes, String username) { - super(); - this.hash = hash; - this.readonly = readonly; - this.hidden = hidden; - this.roles = roles; - this.attributes = attributes; - this.username = username; - } - - public String getUsername() { - return username; - } - - public void setUsername(String username) { - this.username = username; - } - - public InternalUserV6() { - super(); - //default constructor - } - - public String getHash() { - return hash; - } - public void setHash(String hash) { - this.hash = hash; - } - - public void setPassword(String password){ - // no-op setter. Due to a bug in 6.x, empty "password" may be saved to the internalusers doc. Ignore it. - } - - public boolean isReadonly() { - return readonly; - } - public void setReadonly(boolean readonly) { - this.readonly = readonly; - } - public boolean isHidden() { - return hidden; - } - public void setHidden(boolean hidden) { - this.hidden = hidden; - } - public List getRoles() { - return roles; - } - public void setRoles(List roles) { - this.roles = roles; - } - public Map getAttributes() { - return attributes; - } - public void setAttributes(Map attributes) { - this.attributes = attributes; - } - - @Override - public String toString() { - return "SgInternalUser [hash=" + hash + ", readonly=" + readonly + ", hidden=" + hidden + ", roles=" + roles + ", attributes=" - + attributes + "]"; - } - - @JsonIgnore - public boolean isReserved() { - return readonly; - } - - @Override - @JsonIgnore - public void clearHash() { - hash = ""; - } + private String hash; + private boolean readonly; + private boolean hidden; + private List roles = Collections.emptyList(); + private Map attributes = Collections.emptyMap(); + private String username; + + public InternalUserV6( + String hash, + boolean readonly, + boolean hidden, + List roles, + Map attributes, + String username + ) { + super(); + this.hash = hash; + this.readonly = readonly; + this.hidden = hidden; + this.roles = roles; + this.attributes = attributes; + this.username = username; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public InternalUserV6() { + super(); + // default constructor + } + + public String getHash() { + return hash; + } + + public void setHash(String hash) { + this.hash = hash; + } + + public void setPassword(String password) { + // no-op setter. Due to a bug in 6.x, empty "password" may be saved to the internalusers doc. Ignore it. + } + + public boolean isReadonly() { + return readonly; + } + + public void setReadonly(boolean readonly) { + this.readonly = readonly; + } + + public boolean isHidden() { + return hidden; + } + + public void setHidden(boolean hidden) { + this.hidden = hidden; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public Map getAttributes() { + return attributes; + } + + public void setAttributes(Map attributes) { + this.attributes = attributes; + } + @Override + public String toString() { + return "SgInternalUser [hash=" + + hash + + ", readonly=" + + readonly + + ", hidden=" + + hidden + + ", roles=" + + roles + + ", attributes=" + + attributes + + "]"; + } + @JsonIgnore + public boolean isReserved() { + return readonly; + } + @Override + @JsonIgnore + public void clearHash() { + hash = ""; } + +} diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v6/RoleMappingsV6.java b/src/main/java/org/opensearch/security/securityconf/impl/v6/RoleMappingsV6.java index bb10fd8812..f78c2f4305 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v6/RoleMappingsV6.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v6/RoleMappingsV6.java @@ -41,10 +41,7 @@ public class RoleMappingsV6 extends RoleMappings implements Hideable { private boolean readonly; private boolean hidden; private List backendroles = Collections.emptyList(); - private List andBackendroles= Collections.emptyList(); - - - + private List andBackendroles = Collections.emptyList(); public RoleMappingsV6() { super(); @@ -53,34 +50,51 @@ public RoleMappingsV6() { public boolean isReadonly() { return readonly; } + public void setReadonly(boolean readonly) { this.readonly = readonly; } + public boolean isHidden() { return hidden; } + public void setHidden(boolean hidden) { this.hidden = hidden; } + public List getBackendroles() { return backendroles; } + public void setBackendroles(List backendroles) { this.backendroles = backendroles; } - @JsonProperty(value="and_backendroles") + @JsonProperty(value = "and_backendroles") public List getAndBackendroles() { return andBackendroles; } + public void setAndBackendroles(List andBackendroles) { this.andBackendroles = andBackendroles; } @Override public String toString() { - return "RoleMappings [readonly=" + readonly + ", hidden=" + hidden + ", backendroles=" + backendroles + ", hosts=" + getHosts() + ", users=" - + getUsers() + ", andBackendroles=" + andBackendroles + "]"; + return "RoleMappings [readonly=" + + readonly + + ", hidden=" + + hidden + + ", backendroles=" + + backendroles + + ", hosts=" + + getHosts() + + ", users=" + + getUsers() + + ", andBackendroles=" + + andBackendroles + + "]"; } @JsonIgnore diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v6/RoleV6.java b/src/main/java/org/opensearch/security/securityconf/impl/v6/RoleV6.java index e8254d4530..8bc8b46249 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v6/RoleV6.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v6/RoleV6.java @@ -65,8 +65,6 @@ public Map> getTypes() { private List _fls_; private List _masked_fields_; - - public String get_dls_() { return _dls_; } @@ -84,8 +82,6 @@ public String toString() { return "Index [types=" + types + ", _dls_=" + _dls_ + ", _fls_=" + _fls_ + ", _masked_fields_=" + _masked_fields_ + "]"; } - - } public boolean isReadonly() { @@ -130,7 +126,17 @@ public void setIndices(Map indices) { @Override public String toString() { - return "Role [readonly=" + readonly + ", hidden=" + hidden + ", cluster=" + cluster + ", tenants=" + tenants + ", indices=" + indices + "]"; + return "Role [readonly=" + + readonly + + ", hidden=" + + hidden + + ", cluster=" + + cluster + + ", tenants=" + + tenants + + ", indices=" + + indices + + "]"; } @JsonIgnore diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/ActionGroupsV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/ActionGroupsV7.java index f38978eec2..9ec9c25e5d 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/ActionGroupsV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/ActionGroupsV7.java @@ -38,8 +38,6 @@ public class ActionGroupsV7 implements Hideable, StaticDefinable { - - private boolean reserved; private boolean hidden; @JsonProperty(value = "static") @@ -51,11 +49,12 @@ public class ActionGroupsV7 implements Hideable, StaticDefinable { public ActionGroupsV7() { super(); } + public ActionGroupsV7(String agName, ActionGroupsV6 ag6) { reserved = ag6.isReserved(); hidden = ag6.isHidden(); allowed_actions = ag6.getPermissions(); - type = agName.toLowerCase().contains("cluster")?"cluster":"index"; + type = agName.toLowerCase().contains("cluster") ? "cluster" : "index"; description = "Migrated from v6"; } @@ -64,53 +63,72 @@ public ActionGroupsV7(String key, List allowed_actions) { type = "unknown"; description = "Migrated from v6 (legacy)"; } + public String getType() { return type; } + public void setType(String type) { this.type = type; } + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } - public boolean isReserved() { return reserved; } + public void setReserved(boolean reserved) { this.reserved = reserved; } + public boolean isHidden() { return hidden; } + public void setHidden(boolean hidden) { this.hidden = hidden; } + public List getAllowed_actions() { return allowed_actions; } + public void setAllowed_actions(List allowed_actions) { this.allowed_actions = allowed_actions; } + @JsonProperty(value = "static") public boolean isStatic() { return _static; } + @JsonProperty(value = "static") public void setStatic(boolean _static) { this._static = _static; } + @Override public String toString() { - return "ActionGroupsV7 [reserved=" + reserved + ", hidden=" + hidden + ", _static=" + _static + ", allowed_actions=" + allowed_actions - + ", type=" + type + ", description=" + description + "]"; + return "ActionGroupsV7 [reserved=" + + reserved + + ", hidden=" + + hidden + + ", _static=" + + _static + + ", allowed_actions=" + + allowed_actions + + ", type=" + + type + + ", description=" + + description + + "]"; } - - - } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java index f426af2d70..9052c40cda 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/ConfigV7.java @@ -86,20 +86,29 @@ public ConfigV7(ConfigV6 c6) { dynamic.authc = new Authc(); - dynamic.authc.domains.putAll(c6.dynamic.authc.getDomains().entrySet().stream().collect(Collectors.toMap( - entry -> entry.getKey(), - entry -> new AuthcDomain(entry.getValue())))); + dynamic.authc.domains.putAll( + c6.dynamic.authc.getDomains() + .entrySet() + .stream() + .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthcDomain(entry.getValue()))) + ); dynamic.authz = new Authz(); - dynamic.authz.domains.putAll(c6.dynamic.authz.getDomains().entrySet().stream().collect(Collectors.toMap( - entry -> entry.getKey(), - entry -> new AuthzDomain(entry.getValue())))); + dynamic.authz.domains.putAll( + c6.dynamic.authz.getDomains() + .entrySet() + .stream() + .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthzDomain(entry.getValue()))) + ); dynamic.auth_failure_listeners = new AuthFailureListeners(); - dynamic.auth_failure_listeners.listeners.putAll(c6.dynamic.auth_failure_listeners.getListeners().entrySet().stream().collect(Collectors.toMap( - entry -> entry.getKey(), - entry -> new AuthFailureListener(entry.getValue())))); + dynamic.auth_failure_listeners.listeners.putAll( + c6.dynamic.auth_failure_listeners.getListeners() + .entrySet() + .stream() + .collect(Collectors.toMap(entry -> entry.getKey(), entry -> new AuthFailureListener(entry.getValue()))) + ); } @Override @@ -109,7 +118,6 @@ public String toString() { public static class Dynamic { - public String filtered_alias_mode = "warn"; public boolean disable_rest_auth; public boolean disable_intertransport_auth; @@ -130,8 +138,17 @@ public static class Dynamic { @Override public String toString() { - return "Dynamic [filtered_alias_mode=" + filtered_alias_mode + ", kibana=" + kibana + ", http=" + http + ", authc=" + authc + ", authz=" - + authz + ", on_behalf_of=" + on_behalf_of + "]"; + return "Dynamic [filtered_alias_mode=" + + filtered_alias_mode + + ", kibana=" + + kibana + + ", http=" + + http + + ", authc=" + + authc + + ", authz=" + + authz + + "]"; } } @@ -146,27 +163,35 @@ public static class Kibana { public String server_username = "kibanaserver"; public String opendistro_role = null; public String index = ".kibana"; + @Override public String toString() { - return "Kibana [multitenancy_enabled=" + multitenancy_enabled + ", private_tenant_enabled=" + - private_tenant_enabled + ", default_tenant=" + default_tenant + ", server_username=" + - server_username + ", opendistro_role=" + opendistro_role - + ", index=" + index + "]"; + return "Kibana [multitenancy_enabled=" + + multitenancy_enabled + + ", private_tenant_enabled=" + + private_tenant_enabled + + ", default_tenant=" + + default_tenant + + ", server_username=" + + server_username + + ", opendistro_role=" + + opendistro_role + + ", index=" + + index + + "]"; } - - } public static class Http { public boolean anonymous_auth_enabled = false; public Xff xff = new Xff(); + @Override public String toString() { return "Http [anonymous_auth_enabled=" + anonymous_auth_enabled + ", xff=" + xff + "]"; } - } public static class AuthFailureListeners { @@ -183,7 +208,6 @@ public Map getListeners() { return listeners; } - } public static class AuthFailureListener { @@ -195,8 +219,6 @@ public static class AuthFailureListener { public int max_blocked_clients = 100_000; public int max_tracked_clients = 100_000; - - public AuthFailureListener() { super(); } @@ -225,20 +247,21 @@ public String asJson() { public static class Xff { public boolean enabled = false; public String 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}").toString(); - public String remoteIpHeader="X-Forwarded-For"; + "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}" + ).toString(); + public String remoteIpHeader = "X-Forwarded-For"; + @Override public String toString() { - return "Xff [enabled=" + enabled + ", internalProxies=" + internalProxies + ", remoteIpHeader=" + remoteIpHeader+"]"; + return "Xff [enabled=" + enabled + ", internalProxies=" + internalProxies + ", remoteIpHeader=" + remoteIpHeader + "]"; } - } public static class Authc { @@ -261,17 +284,15 @@ public String toString() { return "Authc [domains=" + domains + "]"; } - - } public static class AuthcDomain { @JsonInclude(JsonInclude.Include.NON_NULL) - public boolean http_enabled= true; + public boolean http_enabled = true; @JsonInclude(JsonInclude.Include.NON_NULL) - public boolean transport_enabled= true; - //public boolean enabled= true; + public boolean transport_enabled = true; + // public boolean enabled= true; public int order = 0; public HttpAuthenticator http_authenticator = new HttpAuthenticator(); public AuthcBackend authentication_backend = new AuthcBackend(); @@ -285,10 +306,10 @@ public AuthcDomain(ConfigV6.AuthcDomain v6) { super(); http_enabled = v6.http_enabled && v6.enabled; transport_enabled = v6.transport_enabled && v6.enabled; -// if(v6.enabled)vv { -// http_enabled = true; -// transport_enabled = true; -// } + // if(v6.enabled)vv { + // http_enabled = true; + // transport_enabled = true; + // } order = v6.order; http_authenticator = new HttpAuthenticator(v6.http_authenticator); authentication_backend = new AuthcBackend(v6.authentication_backend); @@ -297,12 +318,21 @@ public AuthcDomain(ConfigV6.AuthcDomain v6) { @Override public String toString() { - return "AuthcDomain [http_enabled=" + http_enabled + ", transport_enabled=" + transport_enabled + ", order=" + order - + ", http_authenticator=" + http_authenticator + ", authentication_backend=" + authentication_backend + ", description=" - + description + "]"; + return "AuthcDomain [http_enabled=" + + http_enabled + + ", transport_enabled=" + + transport_enabled + + ", order=" + + order + + ", http_authenticator=" + + http_authenticator + + ", authentication_backend=" + + authentication_backend + + ", description=" + + description + + "]"; } - } public static class HttpAuthenticator { @@ -315,14 +345,12 @@ public HttpAuthenticator() { super(); } - public HttpAuthenticator(ConfigV6.HttpAuthenticator v6) { this.challenge = v6.challenge; this.type = v6.type; this.config = v6.config; } - @JsonIgnore public String configAsJson() { try { @@ -332,34 +360,26 @@ public String configAsJson() { } } - @Override public String toString() { return "HttpAuthenticator [challenge=" + challenge + ", type=" + type + ", config=" + config + "]"; } - } public static class AuthzBackend { public String type = "noop"; public Map config = Collections.emptyMap(); - - public AuthzBackend() { super(); } - - public AuthzBackend(ConfigV6.AuthzBackend v6) { this.type = v6.type; this.config = v6.config; } - - @JsonIgnore public String configAsJson() { try { @@ -369,35 +389,26 @@ public String configAsJson() { } } - - @Override public String toString() { return "AuthzBackend [type=" + type + ", config=" + config + "]"; } - } public static class AuthcBackend { public String type = InternalAuthenticationBackend.class.getName(); public Map config = Collections.emptyMap(); - - public AuthcBackend() { super(); } - - public AuthcBackend(ConfigV6.AuthcBackend v6) { this.type = v6.type; this.config = v6.config; } - - @JsonIgnore public String configAsJson() { try { @@ -407,14 +418,11 @@ public String configAsJson() { } } - - @Override public String toString() { return "AuthcBackend [type=" + type + ", config=" + config + "]"; } - } public static class Authz { @@ -436,7 +444,6 @@ public String toString() { return "Authz [domains=" + domains + "]"; } - } public static class AuthzDomain { @@ -460,8 +467,15 @@ public AuthzDomain(ConfigV6.AuthzDomain v6) { @Override public String toString() { - return "AuthzDomain [http_enabled=" + http_enabled + ", transport_enabled=" + transport_enabled - + ", authorization_backend=" + authorization_backend + ", description=" + description + "]"; + return "AuthzDomain [http_enabled=" + + http_enabled + + ", transport_enabled=" + + transport_enabled + + ", authorization_backend=" + + authorization_backend + + ", description=" + + description + + "]"; } } @@ -499,7 +513,7 @@ public void setEncryptionKey(String encryptionKey) { @Override public String toString() { - return "OnBehalfOf [signing_key=" + signingKey + ", encryption_key=" + encryptionKey +"]"; + return "OnBehalfOf [signing_key=" + signingKey + ", encryption_key=" + encryptionKey + "]"; } } diff --git a/src/main/java/org/opensearch/security/securityconf/impl/v7/InternalUserV7.java b/src/main/java/org/opensearch/security/securityconf/impl/v7/InternalUserV7.java index a7aed05cc1..8f10df04a4 100644 --- a/src/main/java/org/opensearch/security/securityconf/impl/v7/InternalUserV7.java +++ b/src/main/java/org/opensearch/security/securityconf/impl/v7/InternalUserV7.java @@ -41,30 +41,38 @@ public class InternalUserV7 implements Hideable, Hashed, StaticDefinable { - private String hash; - private boolean reserved; - private boolean hidden; - private boolean service; - private boolean enabled; - @JsonProperty(value = "static") - private boolean _static; - private List backend_roles = Collections.emptyList(); - private Map attributes = Collections.emptyMap(); - private String description; - private List opendistro_security_roles = Collections.emptyList(); - - private InternalUserV7(String hash, boolean reserved, boolean hidden, List backend_roles, Map attributes) { - super(); - this.hash = hash; - this.reserved = reserved; - this.hidden = hidden; - this.backend_roles = backend_roles; - this.attributes = attributes; - this.enabled = true; - this.service = false; - } - - private InternalUserV7(String hash, boolean reserved, boolean hidden, List backend_roles, Map attributes, Boolean enabled, Boolean service) { + private String hash; + private boolean reserved; + private boolean hidden; + private boolean service; + private boolean enabled; + @JsonProperty(value = "static") + private boolean _static; + private List backend_roles = Collections.emptyList(); + private Map attributes = Collections.emptyMap(); + private String description; + private List opendistro_security_roles = Collections.emptyList(); + + private InternalUserV7(String hash, boolean reserved, boolean hidden, List backend_roles, Map attributes) { + super(); + this.hash = hash; + this.reserved = reserved; + this.hidden = hidden; + this.backend_roles = backend_roles; + this.attributes = attributes; + this.enabled = true; + this.service = false; + } + + private InternalUserV7( + String hash, + boolean reserved, + boolean hidden, + List backend_roles, + Map attributes, + Boolean enabled, + Boolean service + ) { super(); this.hash = hash; this.reserved = reserved; @@ -75,111 +83,129 @@ private InternalUserV7(String hash, boolean reserved, boolean hidden, List