diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/authentication/IgniteAuthenticationProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/authentication/IgniteAuthenticationProcessor.java index 31c72620644b96..5d114634a4f954 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/authentication/IgniteAuthenticationProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/authentication/IgniteAuthenticationProcessor.java @@ -980,19 +980,24 @@ public void checkUserOperation(UserManagementOperation op) throws IgniteAccessCo SecuritySubject subj = ctx.security().securityContext().subject(); - if (subj.type() == REMOTE_NODE) { - throw new IgniteAccessControlException("User management operations initiated on behalf of" + - " the Ignite node are not expected."); - } + try { + if (subj.type() == REMOTE_NODE) { + throw new IgniteAccessControlException("User management operations initiated on behalf of" + + " the Ignite node are not expected."); + } - if (!User.DFAULT_USER_NAME.equals(subj.login()) - && !(UserManagementOperation.OperationType.UPDATE == op.type() && subj.login().equals(op.user().name()))) - throw new IgniteAccessControlException("User management operations are not allowed for user. " + - "[curUser=" + subj.login() + ']'); + if (!User.DFAULT_USER_NAME.equals(subj.login()) + && !(UserManagementOperation.OperationType.UPDATE == op.type() && subj.login().equals(op.user().name()))) + throw new IgniteAccessControlException("User management operations are not allowed for user. " + + "[curUser=" + subj.login() + ']'); - if (op.type() == UserManagementOperation.OperationType.REMOVE - && User.DFAULT_USER_NAME.equals(op.user().name())) - throw new IgniteAccessControlException("Default user cannot be removed."); + if (op.type() == UserManagementOperation.OperationType.REMOVE + && User.DFAULT_USER_NAME.equals(op.user().name())) + throw new IgniteAccessControlException("Default user cannot be removed."); + } + catch (SecurityException e) { + throw new IgniteAccessControlException("Failed to perform user management operation.", e); + } } /** diff --git a/modules/core/src/main/java/org/apache/ignite/internal/processors/security/IgniteSecurityProcessor.java b/modules/core/src/main/java/org/apache/ignite/internal/processors/security/IgniteSecurityProcessor.java index 72cc5466a33aad..3787f3406b1f32 100644 --- a/modules/core/src/main/java/org/apache/ignite/internal/processors/security/IgniteSecurityProcessor.java +++ b/modules/core/src/main/java/org/apache/ignite/internal/processors/security/IgniteSecurityProcessor.java @@ -17,6 +17,7 @@ package org.apache.ignite.internal.processors.security; +import java.net.InetSocketAddress; import java.security.Security; import java.util.Collection; import java.util.Map; @@ -42,7 +43,9 @@ import org.apache.ignite.plugin.security.SecurityCredentials; import org.apache.ignite.plugin.security.SecurityException; import org.apache.ignite.plugin.security.SecurityPermission; +import org.apache.ignite.plugin.security.SecurityPermissionSet; import org.apache.ignite.plugin.security.SecuritySubject; +import org.apache.ignite.plugin.security.SecuritySubjectType; import org.apache.ignite.spi.IgniteNodeValidationResult; import org.apache.ignite.spi.discovery.DiscoveryDataBag; import org.jetbrains.annotations.Nullable; @@ -163,10 +166,8 @@ else if (dfltSecCtx.subject().id().equals(subjId)) else res = secCtxs.computeIfAbsent(subjId, uuid -> nodeSecurityContext(marsh, U.resolveClassLoader(ctx.config()), node)); - if (res == null) { - throw new IllegalStateException("Failed to find security context " + - "for subject with given ID : " + subjId); - } + if (res == null) + res = new UnknownUserSecurityContext(subjId); return withContext(res); } @@ -231,6 +232,11 @@ void restoreDefaultContext() { assert secCtx != null; + if (secCtx instanceof UnknownUserSecurityContext) { + throw new SecurityException("Failed to obtain user security context. The user has probably been deleted" + + " in the middle of Ignite operation execution [subjId=" + secCtx.subject().id() + ']'); + } + secPrc.authorize(name, perm, secCtx); } @@ -425,4 +431,92 @@ private IgniteNodeValidationResult validateSecProcClass(ClusterNode node) { public GridSecurityProcessor securityProcessor() { return secPrc; } + + /** + * Security context implementation that is used to handle situation when user security context cannot be obtained + * based on the user subject ID. It can happen if user information is dropped in the middle of Ignite operation or + * security plugin has incorrect implementation. This implementation throws exception on every attempt to get + * any specific use security information from it. + */ + private static class UnknownUserSecurityContext implements SecurityContext { + /** */ + private static final long serialVersionUID = 0L; + + /** */ + private final SecuritySubject secSubj; + + /** */ + public UnknownUserSecurityContext(UUID id) { + secSubj = new UnknownSecuritySubject(id); + } + + /** {@inheritDoc} */ + @Override public SecuritySubject subject() { + return secSubj; + } + + /** {@inheritDoc} */ + @Override public boolean taskOperationAllowed(String taskClsName, SecurityPermission perm) { + return false; + } + + /** {@inheritDoc} */ + @Override public boolean cacheOperationAllowed(String cacheName, SecurityPermission perm) { + return false; + } + + /** {@inheritDoc} */ + @Override public boolean serviceOperationAllowed(String srvcName, SecurityPermission perm) { + return false; + } + + /** {@inheritDoc} */ + @Override public boolean systemOperationAllowed(SecurityPermission perm) { + return false; + } + + /** */ + private static class UnknownSecuritySubject implements SecuritySubject { + /** */ + private static final long serialVersionUID = 0L; + + /** */ + private final UUID secSubjId; + + /** */ + private final String errMsg; + + /** */ + public UnknownSecuritySubject(UUID secSubjId) { + this.secSubjId = secSubjId; + + errMsg = "Failed to obtain security information for the current user [subjId=" + secSubjId + ']'; + } + + /** {@inheritDoc} */ + @Override public UUID id() { + return secSubjId; + } + + /** {@inheritDoc} */ + @Override public SecuritySubjectType type() { + throw new SecurityException(errMsg); + } + + /** {@inheritDoc} */ + @Override public Object login() { + throw new SecurityException(errMsg); + } + + /** {@inheritDoc} */ + @Override public InetSocketAddress address() { + throw new SecurityException(errMsg); + } + + /** {@inheritDoc} */ + @Override public SecurityPermissionSet permissions() { + throw new SecurityException(errMsg); + } + } + } } diff --git a/modules/core/src/test/java/org/apache/ignite/internal/processors/security/UnknownUserSecurityContextTest.java b/modules/core/src/test/java/org/apache/ignite/internal/processors/security/UnknownUserSecurityContextTest.java new file mode 100644 index 00000000000000..7ea309c72a8865 --- /dev/null +++ b/modules/core/src/test/java/org/apache/ignite/internal/processors/security/UnknownUserSecurityContextTest.java @@ -0,0 +1,169 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.ignite.internal.processors.security; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import org.apache.ignite.Ignite; +import org.apache.ignite.IgniteException; +import org.apache.ignite.Ignition; +import org.apache.ignite.client.IgniteClient; +import org.apache.ignite.client.IgniteClientFuture; +import org.apache.ignite.cluster.ClusterNode; +import org.apache.ignite.cluster.ClusterState; +import org.apache.ignite.compute.ComputeJob; +import org.apache.ignite.compute.ComputeJobAdapter; +import org.apache.ignite.compute.ComputeJobResult; +import org.apache.ignite.compute.ComputeTaskAdapter; +import org.apache.ignite.configuration.ClientConfiguration; +import org.apache.ignite.configuration.ClientConnectorConfiguration; +import org.apache.ignite.configuration.DataRegionConfiguration; +import org.apache.ignite.configuration.DataStorageConfiguration; +import org.apache.ignite.configuration.IgniteConfiguration; +import org.apache.ignite.configuration.ThinClientConfiguration; +import org.apache.ignite.resources.IgniteInstanceResource; +import org.apache.ignite.testframework.GridTestUtils; +import org.apache.ignite.testframework.junits.common.GridCommonAbstractTest; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.junit.Test; + +import static java.util.concurrent.TimeUnit.MILLISECONDS; +import static org.apache.ignite.internal.processors.authentication.AuthenticationProcessorSelfTest.authenticate; +import static org.apache.ignite.internal.processors.authentication.AuthenticationProcessorSelfTest.withSecurityContextOnAllNodes; + +/** Tests scenario when {@link SecurityContext} cannot be obtained on remote node. */ +public class UnknownUserSecurityContextTest extends GridCommonAbstractTest { + /** */ + private static CountDownLatch taskExecutionStartedLatch; + + /** */ + private static CountDownLatch taskExecutionUnblockedLatch; + + /** {@inheritDoc} */ + @Override protected void beforeTest() throws Exception { + super.beforeTest(); + + cleanPersistenceDir(); + } + + /** {@inheritDoc} */ + @Override protected IgniteConfiguration getConfiguration(String igniteInstanceName) throws Exception { + IgniteConfiguration cfg = super.getConfiguration(igniteInstanceName); + + cfg.setAuthenticationEnabled(true); + + cfg.setClientConnectorConfiguration(new ClientConnectorConfiguration() + .setThinClientConfiguration(new ThinClientConfiguration() + .setMaxActiveComputeTasksPerConnection(1))); + + cfg.setDataStorageConfiguration(new DataStorageConfiguration() + .setDefaultDataRegionConfiguration(new DataRegionConfiguration() + .setPersistenceEnabled(true))); + + return cfg; + } + + /** */ + @Test + public void testUserDropDuringOperation() throws Exception { + startGrids(2); + + grid(0).cluster().state(ClusterState.ACTIVE); + + grid(0).createCache(DEFAULT_CACHE_NAME); + + String user = "cli"; + String pwd = "pwd"; + + try (AutoCloseable ignored = withSecurityContextOnAllNodes(authenticate(grid(0), "ignite", "ignite"))) { + grid(0).context().security().createUser(user, pwd.toCharArray()); + } + + IgniteClient cli = Ignition.startClient(new ClientConfiguration().setAddresses("127.0.0.1:10800") + .setUserName(user) + .setUserPassword(pwd)); + + taskExecutionStartedLatch = new CountDownLatch(1); + taskExecutionUnblockedLatch = new CountDownLatch(1); + + IgniteClientFuture fut = cli.compute().executeAsync2(TestTask.class.getName(), grid(1).localNode().id()); + + taskExecutionStartedLatch.await(); + + try (AutoCloseable ignored = withSecurityContextOnAllNodes(authenticate(grid(0), "ignite", "ignite"))) { + grid(0).context().security().dropUser(user); + } + + taskExecutionUnblockedLatch.countDown(); + + GridTestUtils.assertThrowsAnyCause( + log, () -> fut.get(getTestTimeout(), MILLISECONDS), + ExecutionException.class, + "Failed to obtain user security context. The user has probably been deleted in the middle of Ignite operation execution"); + } + + /** */ + public static class TestTask extends ComputeTaskAdapter { + /** {@inheritDoc} */ + @Override public @NotNull Map map( + List subgrid, + @Nullable UUID nodeId + ) throws IgniteException { + taskExecutionStartedLatch.countDown(); + + try { + taskExecutionUnblockedLatch.await(); + } + catch (InterruptedException e) { + throw new IgniteException(e); + } + + for (ClusterNode node : subgrid) { + if (node.id().equals(nodeId)) { + return Collections.singletonMap(new ComputeJobAdapter() { + @IgniteInstanceResource + private Ignite ignite; + + @Override public void cancel() { + // No-op. + } + + @Override public Serializable execute() { + ignite.cache(DEFAULT_CACHE_NAME).put("key", "val"); + + return null; + } + }, node); + } + } + + return null; + } + + /** {@inheritDoc} */ + @Override public @Nullable Void reduce(List results) throws IgniteException { + return null; + } + } +} diff --git a/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java b/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java index 5400f0d52ddb2e..eb53b8c2868862 100644 --- a/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java +++ b/modules/core/src/test/java/org/apache/ignite/testsuites/SecurityTestSuite.java @@ -19,6 +19,7 @@ import org.apache.ignite.internal.processors.security.IgniteSecurityProcessorTest; import org.apache.ignite.internal.processors.security.InvalidServerTest; +import org.apache.ignite.internal.processors.security.UnknownUserSecurityContextTest; import org.apache.ignite.internal.processors.security.cache.CacheOperationPermissionCheckTest; import org.apache.ignite.internal.processors.security.cache.CacheOperationPermissionCreateDestroyCheckTest; import org.apache.ignite.internal.processors.security.cache.ContinuousQueryPermissionCheckTest; @@ -132,7 +133,8 @@ MaintenanceModeNodeSecurityTest.class, DaemonNodeBasicSecurityTest.class, ServiceAuthorizationTest.class, - ServiceStaticConfigTest.class + ServiceStaticConfigTest.class, + UnknownUserSecurityContextTest.class }) public class SecurityTestSuite { /** */