From 7fe2a4dd3f0d67c80eaffff4cded72c51990ce43 Mon Sep 17 00:00:00 2001 From: Anatolii Bazko Date: Mon, 4 Dec 2023 15:55:35 +0100 Subject: [PATCH] feat: Advanced authorization (#619) * feat: Advanced authorization Signed-off-by: Anatolii Bazko * Renames fields Signed-off-by: Anatolii Bazko * Address remarks Signed-off-by: Anatolii Bazko * Address remarks Signed-off-by: Anatolii Bazko * Address remarks Signed-off-by: Anatolii Bazko --------- Signed-off-by: Anatolii Bazko --- .../webapp/WEB-INF/classes/che/che.properties | 13 ++ .../eclipse/che/commons/lang/StringUtils.java | 18 ++- .../kubernetes/KubernetesInfraModule.java | 6 + .../authorization/AuthorizationChecker.java | 20 +++ .../authorization/AuthorizationException.java | 27 ++++ .../KubernetesAuthorizationCheckerImpl.java | 48 ++++++ .../authorization/PermissionsCleaner.java | 46 ++++++ .../namespace/KubernetesNamespaceFactory.java | 35 +++- .../KubernetesAuthorizationCheckerTest.java | 49 ++++++ .../KubernetesNamespaceFactoryTest.java | 153 +++++++++++++----- infrastructures/openshift/pom.xml | 11 ++ .../openshift/OpenShiftInfraModule.java | 6 + .../OpenShiftAuthorizationCheckerImpl.java | 97 +++++++++++ .../project/OpenShiftProjectFactory.java | 11 +- .../OpenShiftAuthorizationCheckerTest.java | 119 ++++++++++++++ .../project/OpenShiftProjectFactoryTest.java | 41 +++++ 16 files changed, 660 insertions(+), 40 deletions(-) create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/AuthorizationChecker.java create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/AuthorizationException.java create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/KubernetesAuthorizationCheckerImpl.java create mode 100644 infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/PermissionsCleaner.java create mode 100644 infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/KubernetesAuthorizationCheckerTest.java create mode 100644 infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/authorization/OpenShiftAuthorizationCheckerImpl.java create mode 100644 infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/authorization/OpenShiftAuthorizationCheckerTest.java diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties index 8c660ec9363..8ee1f1f10bc 100644 --- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties +++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/classes/che/che.properties @@ -663,3 +663,16 @@ che.oauth2.gitlab.clientid_filepath=NULL # Location of the file with GitLab client secret. che.oauth2.gitlab.clientsecret_filepath=NULL + +### Advanced authorization +# Comma separated list of users allowed to access Che. +che.infra.kubernetes.advanced_authorization.allow_users=NULL + +# Comma separated list of groups of users allowed to access Che. +che.infra.kubernetes.advanced_authorization.allow_groups=NULL + +# Comma separated list of users denied to access Che. +che.infra.kubernetes.advanced_authorization.deny_users=NULL + +# Comma separated list of groups of users denied to access Che. +che.infra.kubernetes.advanced_authorization.deny_groups=NULL diff --git a/core/commons/che-core-commons-lang/src/main/java/org/eclipse/che/commons/lang/StringUtils.java b/core/commons/che-core-commons-lang/src/main/java/org/eclipse/che/commons/lang/StringUtils.java index a83fde764f4..407d921293a 100644 --- a/core/commons/che-core-commons-lang/src/main/java/org/eclipse/che/commons/lang/StringUtils.java +++ b/core/commons/che-core-commons-lang/src/main/java/org/eclipse/che/commons/lang/StringUtils.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012-2018 Red Hat, Inc. + * Copyright (c) 2012-2023 Red Hat, Inc. * This program and the accompanying materials are made * available under the terms of the Eclipse Public License 2.0 * which is available at https://www.eclipse.org/legal/epl-2.0/ @@ -11,6 +11,13 @@ */ package org.eclipse.che.commons.lang; +import static com.google.common.base.Strings.isNullOrEmpty; + +import com.google.common.base.Splitter; +import com.google.common.collect.Sets; +import java.util.Collections; +import java.util.Set; + /** Set of useful String methods */ public class StringUtils { @@ -91,4 +98,13 @@ public static int lastIndexOf(CharSequence s, char c, int start, int end) { } return -1; } + + /** Parse string to set of strings. String should be comma separated. Whitespaces are trimmed. */ + public static Set strToSet(String str) { + if (!isNullOrEmpty(str)) { + return Sets.newHashSet(Splitter.on(",").trimResults().omitEmptyStrings().split(str)); + } else { + return Collections.emptySet(); + } + } } diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java index 1b5678ef1ec..8fcc1be4fc9 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/KubernetesInfraModule.java @@ -36,6 +36,9 @@ import org.eclipse.che.api.workspace.server.wsplugins.ChePluginsApplier; import org.eclipse.che.api.workspace.shared.Constants; import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.KubernetesNamespaceService; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.AuthorizationChecker; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.KubernetesAuthorizationCheckerImpl; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.PermissionsCleaner; import org.eclipse.che.workspace.infrastructure.kubernetes.cache.jpa.JpaKubernetesRuntimeCacheModule; import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.DockerimageComponentToWorkspaceApplier; import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.KubernetesComponentToWorkspaceApplier; @@ -110,6 +113,9 @@ protected void configure() { namespaceConfigurators.addBinding().to(SshKeysConfigurator.class); namespaceConfigurators.addBinding().to(GitconfigUserDataConfigurator.class); + bind(AuthorizationChecker.class).to(KubernetesAuthorizationCheckerImpl.class); + bind(PermissionsCleaner.class).asEagerSingleton(); + bind(KubernetesNamespaceService.class); MapBinder factories = diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/AuthorizationChecker.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/AuthorizationChecker.java new file mode 100644 index 00000000000..73b238c1dec --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/AuthorizationChecker.java @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2012-2023 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.authorization; + +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; + +/** This {@link AuthorizationChecker} checks if user is allowed to use Che. */ +public interface AuthorizationChecker { + + boolean isAuthorized(String username) throws InfrastructureException; +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/AuthorizationException.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/AuthorizationException.java new file mode 100644 index 00000000000..eb08dbe73b1 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/AuthorizationException.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2012-2023 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.authorization; + +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.api.workspace.server.spi.RuntimeInfrastructure; + +/** + * An exception thrown by {@link RuntimeInfrastructure} and related components. Indicates that a + * user is not authorized to use Che. + * + * @author Anatolii Bazko + */ +public class AuthorizationException extends InfrastructureException { + public AuthorizationException(String message) { + super(message); + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/KubernetesAuthorizationCheckerImpl.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/KubernetesAuthorizationCheckerImpl.java new file mode 100644 index 00000000000..16f9893ea5d --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/KubernetesAuthorizationCheckerImpl.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2012-2023 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.authorization; + +import static org.eclipse.che.commons.lang.StringUtils.strToSet; + +import java.util.Set; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import org.eclipse.che.commons.annotation.Nullable; + +/** This {@link KubernetesAuthorizationCheckerImpl} checks if user is allowed to use Che. */ +@Singleton +public class KubernetesAuthorizationCheckerImpl implements AuthorizationChecker { + + private final Set allowUsers; + private final Set denyUsers; + + @Inject + public KubernetesAuthorizationCheckerImpl( + @Nullable @Named("che.infra.kubernetes.advanced_authorization.allow_users") String allowUsers, + @Nullable @Named("che.infra.kubernetes.advanced_authorization.deny_users") String denyUsers) { + this.allowUsers = strToSet(allowUsers); + this.denyUsers = strToSet(denyUsers); + } + + public boolean isAuthorized(String username) { + return isAllowedUser(username) && !isDeniedUser(username); + } + + private boolean isAllowedUser(String username) { + return allowUsers.isEmpty() || allowUsers.contains(username); + } + + private boolean isDeniedUser(String username) { + return !denyUsers.isEmpty() && denyUsers.contains(username); + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/PermissionsCleaner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/PermissionsCleaner.java new file mode 100644 index 00000000000..8b4eacaf981 --- /dev/null +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/PermissionsCleaner.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2012-2023 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.authorization; + +import static org.eclipse.che.commons.lang.StringUtils.strToSet; + +import io.fabric8.kubernetes.client.KubernetesClient; +import java.util.Set; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory; + +/** This {@link PermissionsCleaner} cleans up all user's permissions. */ +@Singleton +public class PermissionsCleaner { + + private final Set userClusterRoles; + private final CheServerKubernetesClientFactory cheServerKubernetesClientFactory; + + @Inject + public PermissionsCleaner( + @Nullable @Named("che.infra.kubernetes.user_cluster_roles") String userClusterRoles, + CheServerKubernetesClientFactory cheServerKubernetesClientFactory) { + this.cheServerKubernetesClientFactory = cheServerKubernetesClientFactory; + this.userClusterRoles = strToSet(userClusterRoles); + } + + public void cleanUp(String namespaceName) throws InfrastructureException { + KubernetesClient client = cheServerKubernetesClientFactory.create(); + for (String userClusterRole : userClusterRoles) { + client.rbac().roleBindings().inNamespace(namespaceName).withName(userClusterRole).delete(); + } + } +} diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java index a76a7b42078..319d1b8770e 100644 --- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java +++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactory.java @@ -57,6 +57,9 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.impls.KubernetesNamespaceMetaImpl; import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.AuthorizationChecker; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.AuthorizationException; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.PermissionsCleaner; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator; import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool; import org.slf4j.Logger; @@ -98,6 +101,8 @@ public class KubernetesNamespaceFactory { private final PreferenceManager preferenceManager; protected final Set namespaceConfigurators; protected final KubernetesSharedPool sharedPool; + protected final AuthorizationChecker authorizationChecker; + protected final PermissionsCleaner permissionsCleaner; @Inject public KubernetesNamespaceFactory( @@ -110,7 +115,9 @@ public KubernetesNamespaceFactory( Set namespaceConfigurators, CheServerKubernetesClientFactory cheServerKubernetesClientFactory, PreferenceManager preferenceManager, - KubernetesSharedPool sharedPool) + KubernetesSharedPool sharedPool, + AuthorizationChecker authorizationChecker, + PermissionsCleaner permissionsCleaner) throws ConfigurationException { this.namespaceCreationAllowed = namespaceCreationAllowed; this.cheServerKubernetesClientFactory = cheServerKubernetesClientFactory; @@ -120,6 +127,8 @@ public KubernetesNamespaceFactory( this.labelNamespaces = labelNamespaces; this.annotateNamespaces = annotateNamespaces; this.namespaceConfigurators = ImmutableSet.copyOf(namespaceConfigurators); + this.authorizationChecker = authorizationChecker; + this.permissionsCleaner = permissionsCleaner; //noinspection UnstableApiUsage Splitter.MapSplitter csvMapSplitter = Splitter.on(",").withKeyValueSeparator("="); @@ -281,6 +290,9 @@ public KubernetesNamespace getOrCreate(RuntimeIdentity identity) throws Infrastr var subject = EnvironmentContext.getCurrent().getSubject(); var userName = subject.getUserName(); + + validateAuthorization(namespace.getName(), userName); + NamespaceResolutionContext resolutionCtx = new NamespaceResolutionContext(identity.getWorkspaceId(), subject.getUserId(), userName); Map namespaceAnnotationsEvaluated = @@ -573,6 +585,27 @@ public void deleteIfManaged(Workspace workspace) throws InfrastructureException } } + protected void validateAuthorization(String namespaceName, String username) + throws InfrastructureException { + if (!authorizationChecker.isAuthorized(username)) { + try { + permissionsCleaner.cleanUp(namespaceName); + } catch (InfrastructureException | KubernetesClientException e) { + LOG.error( + "Failed to clean up permissions for user '{}' in namespace '{}'. Cause: {}", + username, + namespaceName, + e.getMessage(), + e); + } + + throw new AuthorizationException( + format( + "User '%s' is not authorized to create a project. Please contact your system administrator.", + username)); + } + } + protected String evalPlaceholders(String namespace, NamespaceResolutionContext ctx) { checkArgument(!isNullOrEmpty(namespace)); String evaluated = namespace; diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/KubernetesAuthorizationCheckerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/KubernetesAuthorizationCheckerTest.java new file mode 100644 index 00000000000..3c5d0a5b4a4 --- /dev/null +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/authorization/KubernetesAuthorizationCheckerTest.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2012-2023 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.kubernetes.authorization; + +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class KubernetesAuthorizationCheckerTest { + @Test(dataProvider = "advancedAuthorizationData") + public void advancedAuthorization( + String testUserName, String allowedUsers, String deniedUsers, boolean expectedIsAuthorized) + throws InfrastructureException { + // give + AuthorizationChecker authorizationChecker = + new KubernetesAuthorizationCheckerImpl(allowedUsers, deniedUsers); + + // when + boolean isAuthorized = authorizationChecker.isAuthorized(testUserName); + + // then + Assert.assertEquals(isAuthorized, expectedIsAuthorized); + } + + @DataProvider + public static Object[][] advancedAuthorizationData() { + return new Object[][] { + {"user1", "", "", true}, + {"user1", "user1", "", true}, + {"user1", "user1", "user2", true}, + {"user1", "user1", "user1", false}, + {"user2", "user1", "", false}, + {"user2", "user1", "user2", false}, + }; + } +} diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java index 061c06aaf98..689481e017f 100644 --- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java +++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/namespace/KubernetesNamespaceFactoryTest.java @@ -87,6 +87,8 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.impls.KubernetesNamespaceMetaImpl; import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.AuthorizationChecker; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.PermissionsCleaner; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.PreferencesConfigMapConfigurator; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.WorkspaceServiceAccountConfigurator; @@ -126,6 +128,8 @@ public class KubernetesNamespaceFactoryTest { private KubernetesClient k8sClient; @Mock private PreferenceManager preferenceManager; @Mock Appender mockedAppender; + @Mock AuthorizationChecker authorizationChecker; + @Mock PermissionsCleaner permissionsCleaner; @Mock private NonNamespaceOperation namespaceOperation; @@ -151,6 +155,7 @@ public void setUp() throws Exception { lenient().when(namespaceOperation.withName(any())).thenReturn(namespaceResource); lenient().when(namespaceResource.get()).thenReturn(mock(Namespace.class)); + lenient().when(authorizationChecker.isAuthorized(anyString())).thenReturn(true); lenient().doReturn(namespaceListResource).when(namespaceOperation).withLabels(anyMap()); lenient().when(namespaceListResource.list()).thenReturn(namespaceList); @@ -179,7 +184,9 @@ public void shouldNotThrowExceptionIfDefaultNamespaceIsSpecifiedOnCheckingIfName emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); namespaceFactory.checkIfNamespaceIsAllowed("jondoe-che"); } @@ -204,7 +211,9 @@ public void shouldLookAtStoredNamespacesOnCheckingIfNamespaceIsAllowed() throws emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); namespaceFactory.checkIfNamespaceIsAllowed("any-namespace"); } @@ -222,7 +231,9 @@ public void shouldNormaliseNamespaceWhenUserNameStartsWithKube() { emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); assertEquals("che-kube-admin", namespaceFactory.normalizeNamespaceName("kube:admin")); } @@ -245,7 +256,9 @@ public void shouldNormaliseNamespaceWhenUserNameStartsWithKube() { emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); namespaceFactory.checkIfNamespaceIsAllowed("any-namespace"); } @@ -265,7 +278,9 @@ public void shouldThrowExceptionIfNoDefaultNamespaceIsConfigured() { emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); } @Test @@ -313,7 +328,9 @@ public void shouldReturnPreparedNamespacesWhenFound() throws InfrastructureExcep emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "123", null, false)); // when @@ -354,7 +371,9 @@ public void shouldNotThrowAnExceptionWhenNotAllowedToListNamespaces() throws Exc emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "123", null, false)); // when @@ -382,7 +401,9 @@ public void throwAnExceptionWhenErrorListingNamespaces() throws Exception { emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); // when namespaceFactory.list(); @@ -413,7 +434,9 @@ public void shouldReturnDefaultNamespaceWhenItExists() throws Exception { emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); List availableNamespaces = namespaceFactory.list(); assertEquals(availableNamespaces.size(), 1); @@ -439,7 +462,9 @@ public void shouldReturnDefaultNamespaceWhenItDoesNotExistAndUserDefinedIsNotAll emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); List availableNamespaces = namespaceFactory.list(); assertEquals(availableNamespaces.size(), 1); @@ -467,7 +492,9 @@ public void shouldCreatePreferencesConfigmapIfNotExists() throws Exception { Set.of(new PreferencesConfigMapConfigurator(cheServerKubernetesClientFactory)), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); when(toReturnNamespace.getName()).thenReturn("namespaceName"); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); @@ -508,7 +535,9 @@ public void testAllConfiguratorsAreCalledWhenCreatingNamespace() throws Infrastr namespaceConfigurators, cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "123", null, false)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); @@ -543,7 +572,9 @@ public void shouldNotCreateCredentialsSecretIfExists() throws Exception { emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); prepareNamespace(toReturnNamespace); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); @@ -575,7 +606,9 @@ public void shouldNotCreatePreferencesConfigmapIfExists() throws Exception { emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); prepareNamespace(toReturnNamespace); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); @@ -608,7 +641,9 @@ public void shouldThrowExceptionWhenFailedToGetInfoAboutDefaultNamespace() throw emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); throwOnTryToGetNamespaceByName( "jondoe-che", new KubernetesClientException("connection refused")); @@ -631,7 +666,9 @@ public void shouldThrowExceptionWhenFailedToGetNamespaces() throws Exception { emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); throwOnTryToGetNamespacesList(new KubernetesClientException("connection refused")); namespaceFactory.list(); @@ -659,7 +696,9 @@ public void shouldRequireNamespacePriorExistenceIfDifferentFromDefaultAndUserDef emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); prepareNamespace(toReturnNamespace); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); @@ -688,7 +727,9 @@ public void shouldReturnDefaultNamespaceWhenCreatingIsNotIsNotAllowed() throws E emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); prepareNamespace(toReturnNamespace); doReturn(toReturnNamespace).when(namespaceFactory).doCreateNamespaceAccess(any(), any()); @@ -723,7 +764,9 @@ public void shouldPrepareWorkspaceServiceAccountIfItIsConfiguredAndNamespaceIsNo Set.of(serviceAccountCfg), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); prepareNamespace(toReturnNamespace); when(toReturnNamespace.getName()).thenReturn("workspace123"); @@ -761,7 +804,9 @@ public void shouldBindToAllConfiguredClusterRoles() throws Exception { Set.of(serviceAccountConfigurator), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); prepareNamespace(toReturnNamespace); when(toReturnNamespace.getName()).thenReturn("workspace123"); @@ -836,7 +881,9 @@ public void shouldCreateAndBindCredentialsSecretRole() throws Exception { Set.of(serviceAccountConfigurator), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); prepareNamespace(toReturnNamespace); when(toReturnNamespace.getName()).thenReturn("workspace123"); @@ -881,7 +928,9 @@ public void shouldCreateExecAndViewRolesAndBindings() throws Exception { "serviceAccount", "", cheServerKubernetesClientFactory)), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); prepareNamespace(toReturnNamespace); when(toReturnNamespace.getName()).thenReturn("workspace123"); @@ -945,7 +994,9 @@ public void shouldCreateExecAndViewRolesAndBindings() throws Exception { emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); WorkspaceImpl workspace = new WorkspaceImplBuilder().setId("workspace123").setAttributes(emptyMap()).build(); @@ -968,7 +1019,9 @@ public void testEvalNamespaceUsesNamespaceFromUserPreferencesIfExist() throws Ex emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); Map prefs = new HashMap<>(); prefs.put(WORKSPACE_INFRASTRUCTURE_NAMESPACE_ATTRIBUTE, "che-123"); @@ -996,7 +1049,9 @@ public void testEvalNamespaceSkipsNamespaceFromUserPreferencesIfTemplateChanged( emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); Map prefs = new HashMap<>(); // returned but ignored @@ -1025,7 +1080,9 @@ public void testEvalNamespaceSkipsNamespaceFromUserPreferencesIfUserAllowedPrope emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); Map prefs = new HashMap<>(); // returned but ignored @@ -1054,7 +1111,9 @@ public void testEvalNamespaceKubeAdmin() throws Exception { emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); doReturn(empty()).when(namespaceFactory).fetchNamespace(anyString()); String namespace = @@ -1079,7 +1138,9 @@ public void testEvalNamespaceUsesWorkspaceRecordedNamespaceIfWorkspaceRecordsIt( emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); WorkspaceImpl workspace = new WorkspaceImplBuilder() @@ -1106,7 +1167,9 @@ public void testEvalNamespaceTreatsWorkspaceRecordedNamespaceLiterally() throws emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); WorkspaceImpl workspace = new WorkspaceImplBuilder() @@ -1154,7 +1217,9 @@ public void testEvalNamespaceNameWhenPreparedNamespacesFound() throws Infrastruc emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); String namespace = namespaceFactory.evaluateNamespaceName( @@ -1178,7 +1243,9 @@ public void shouldHandleProvision() throws InfrastructureException { emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); prepareNamespace(toReturnNamespace); when(toReturnNamespace.getName()).thenReturn("jondoe-che"); @@ -1215,7 +1282,9 @@ public void shouldFailToProvisionIfNotAbleToFindNamespace() throws Infrastructur emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); prepareNamespace(toReturnNamespace); when(toReturnNamespace.getName()).thenReturn("jondoe-cha-cha-cha"); @@ -1251,7 +1320,9 @@ public void shouldFail2ProvisionIfNotAbleToFindNamespace() throws Infrastructure emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); prepareNamespace(toReturnNamespace); when(toReturnNamespace.getName()).thenReturn("jondoe-cha-cha-cha"); @@ -1299,7 +1370,9 @@ public void testUsernamePlaceholderInLabelsIsNotEvaluated() throws Infrastructur emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "123", null, false)); namespaceFactory.list(); @@ -1322,7 +1395,9 @@ public void testUsernamePlaceholderInAnnotationsIsEvaluated() throws Infrastruct emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool)); + pool, + authorizationChecker, + permissionsCleaner)); EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "123", null, false)); KubernetesNamespace toReturnNamespace = mock(KubernetesNamespace.class); prepareNamespace(toReturnNamespace); @@ -1351,7 +1426,9 @@ public void normalizeTest(String raw, String expected) { emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); assertEquals(expected, namespaceFactory.normalizeNamespaceName(raw)); } @@ -1368,7 +1445,9 @@ public void normalizeLengthTest() { emptySet(), cheServerKubernetesClientFactory, preferenceManager, - pool); + pool, + authorizationChecker, + permissionsCleaner); assertEquals( 63, diff --git a/infrastructures/openshift/pom.xml b/infrastructures/openshift/pom.xml index bed24d811f1..f3e3bcee194 100644 --- a/infrastructures/openshift/pom.xml +++ b/infrastructures/openshift/pom.xml @@ -161,6 +161,17 @@ mockwebserver test + + io.fabric8 + kubernetes-server-mock + test + + + junit-jupiter-api + org.junit.jupiter + + + io.fabric8 openshift-server-mock diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java index 3b7ab5c5387..4ec75404dd2 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/OpenShiftInfraModule.java @@ -40,6 +40,8 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesEnvironmentProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.StartSynchronizerFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.KubernetesNamespaceService; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.AuthorizationChecker; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.PermissionsCleaner; import org.eclipse.che.workspace.infrastructure.kubernetes.cache.jpa.JpaKubernetesRuntimeCacheModule; import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.DockerimageComponentToWorkspaceApplier; import org.eclipse.che.workspace.infrastructure.kubernetes.devfile.KubernetesComponentToWorkspaceApplier; @@ -80,6 +82,7 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.wsplugins.SidecarToolingProvisioner; import org.eclipse.che.workspace.infrastructure.kubernetes.wsplugins.brokerphases.BrokerEnvironmentFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.wsplugins.events.BrokerService; +import org.eclipse.che.workspace.infrastructure.openshift.authorization.OpenShiftAuthorizationCheckerImpl; import org.eclipse.che.workspace.infrastructure.openshift.devfile.OpenshiftComponentToWorkspaceApplier; import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment; import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironmentFactory; @@ -118,6 +121,9 @@ protected void configure() { namespaceConfigurators.addBinding().to(SshKeysConfigurator.class); namespaceConfigurators.addBinding().to(GitconfigUserDataConfigurator.class); + bind(AuthorizationChecker.class).to(OpenShiftAuthorizationCheckerImpl.class); + bind(PermissionsCleaner.class).asEagerSingleton(); + bind(KubernetesNamespaceService.class); MapBinder factories = diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/authorization/OpenShiftAuthorizationCheckerImpl.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/authorization/OpenShiftAuthorizationCheckerImpl.java new file mode 100644 index 00000000000..a606e4f5126 --- /dev/null +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/authorization/OpenShiftAuthorizationCheckerImpl.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2012-2023 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.openshift.authorization; + +import static org.eclipse.che.commons.lang.StringUtils.strToSet; + +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.openshift.api.model.Group; +import java.util.Set; +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.commons.annotation.Nullable; +import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.AuthorizationChecker; + +/** This {@link OpenShiftAuthorizationCheckerImpl} checks if user is allowed to use Che. */ +@Singleton +public class OpenShiftAuthorizationCheckerImpl implements AuthorizationChecker { + + private final CheServerKubernetesClientFactory cheServerKubernetesClientFactory; + + private final Set allowUsers; + private final Set allowGroups; + private final Set denyUsers; + private final Set denyGroups; + + @Inject + public OpenShiftAuthorizationCheckerImpl( + @Nullable @Named("che.infra.kubernetes.advanced_authorization.allow_users") String allowUsers, + @Nullable @Named("che.infra.kubernetes.advanced_authorization.allow_groups") + String allowGroups, + @Nullable @Named("che.infra.kubernetes.advanced_authorization.deny_users") String denyUsers, + @Nullable @Named("che.infra.kubernetes.advanced_authorization.deny_groups") String denyGroups, + CheServerKubernetesClientFactory cheServerKubernetesClientFactory) { + this.allowUsers = strToSet(allowUsers); + this.allowGroups = strToSet(allowGroups); + this.denyUsers = strToSet(denyUsers); + this.denyGroups = strToSet(denyGroups); + this.cheServerKubernetesClientFactory = cheServerKubernetesClientFactory; + } + + public boolean isAuthorized(String username) throws InfrastructureException { + return isAllowedUser(cheServerKubernetesClientFactory.create(), username) + && !isDeniedUser(cheServerKubernetesClientFactory.create(), username); + } + + private boolean isAllowedUser(KubernetesClient client, String username) { + // All users from all groups are allowed by default + if (allowUsers.isEmpty() && allowGroups.isEmpty()) { + return true; + } + + if (allowUsers.contains(username)) { + return true; + } + + for (String groupName : allowGroups) { + Group group = client.resources(Group.class).withName(groupName).get(); + if (group != null && group.getUsers().contains(username)) { + return true; + } + } + + return false; + } + + private boolean isDeniedUser(KubernetesClient client, String username) { + // All users from all groups are allowed by default + if (denyUsers.isEmpty() && denyGroups.isEmpty()) { + return false; + } + + if (denyUsers.contains(username)) { + return true; + } + + for (String groupName : denyGroups) { + Group group = client.resources(Group.class).withName(groupName).get(); + if (group != null && group.getUsers().contains(username)) { + return true; + } + } + + return false; + } +} diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java index 7ae64d5574c..bc1976b2f33 100644 --- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java +++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactory.java @@ -40,6 +40,8 @@ import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.api.server.impls.KubernetesNamespaceMetaImpl; import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.AuthorizationChecker; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.PermissionsCleaner; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesNamespaceFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator; import org.eclipse.che.workspace.infrastructure.kubernetes.util.KubernetesSharedPool; @@ -81,6 +83,8 @@ public OpenShiftProjectFactory( CheServerOpenshiftClientFactory cheServerOpenshiftClientFactory, PreferenceManager preferenceManager, KubernetesSharedPool sharedPool, + AuthorizationChecker authorizationChecker, + PermissionsCleaner permissionsCleaner, @Nullable @Named("che.infra.openshift.oauth_identity_provider") String oAuthIdentityProvider) { super( @@ -93,7 +97,9 @@ public OpenShiftProjectFactory( namespaceConfigurators, cheServerKubernetesClientFactory, preferenceManager, - sharedPool); + sharedPool, + authorizationChecker, + permissionsCleaner); this.initWithCheServerSa = initWithCheServerSa; this.cheServerKubernetesClientFactory = cheServerKubernetesClientFactory; this.cheServerOpenshiftClientFactory = cheServerOpenshiftClientFactory; @@ -105,6 +111,9 @@ public OpenShiftProject getOrCreate(RuntimeIdentity identity) throws Infrastruct OpenShiftProject osProject = get(identity); var subject = EnvironmentContext.getCurrent().getSubject(); var userName = subject.getUserName(); + + validateAuthorization(osProject.getName(), userName); + NamespaceResolutionContext resolutionCtx = new NamespaceResolutionContext(identity.getWorkspaceId(), subject.getUserId(), userName); Map namespaceAnnotationsEvaluated = diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/authorization/OpenShiftAuthorizationCheckerTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/authorization/OpenShiftAuthorizationCheckerTest.java new file mode 100644 index 00000000000..f826cf40029 --- /dev/null +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/authorization/OpenShiftAuthorizationCheckerTest.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2012-2023 Red Hat, Inc. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Red Hat, Inc. - initial API and implementation + */ +package org.eclipse.che.workspace.infrastructure.openshift.authorization; + +import static org.mockito.Mockito.*; + +import io.fabric8.kubernetes.api.model.ObjectMetaBuilder; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.server.mock.KubernetesServer; +import io.fabric8.openshift.api.model.Group; +import java.util.Collections; +import java.util.List; +import org.eclipse.che.api.workspace.server.spi.InfrastructureException; +import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory; +import org.mockito.Mock; +import org.mockito.testng.MockitoTestNGListener; +import org.testng.Assert; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +@Listeners(MockitoTestNGListener.class) +public class OpenShiftAuthorizationCheckerTest { + + @Mock private CheServerKubernetesClientFactory clientFactory; + private KubernetesClient client; + private KubernetesServer serverMock; + + @BeforeMethod + public void setUp() throws InfrastructureException { + serverMock = new KubernetesServer(true, true); + serverMock.before(); + client = spy(serverMock.getClient()); + lenient().when(clientFactory.create()).thenReturn(client); + } + + @Test(dataProvider = "advancedAuthorizationData") + public void advancedAuthorization( + String testUserName, + List groups, + String allowedUsers, + String allowedGroups, + String deniedUsers, + String deniedGroups, + boolean expectedIsAuthorized) + throws InfrastructureException { + // give + OpenShiftAuthorizationCheckerImpl authorizationChecker = + new OpenShiftAuthorizationCheckerImpl( + allowedUsers, allowedGroups, deniedUsers, deniedGroups, clientFactory); + groups.forEach(group -> client.resources(Group.class).create(group)); + + // when + boolean isAuthorized = authorizationChecker.isAuthorized(testUserName); + + // then + Assert.assertEquals(isAuthorized, expectedIsAuthorized); + } + + @DataProvider + public static Object[][] advancedAuthorizationData() { + Group groupWithUser1 = + new Group( + "v1", + "Group", + new ObjectMetaBuilder().withName("groupWithUser1").build(), + List.of("user1")); + Group groupWithUser2 = + new Group( + "v1", + "Group", + new ObjectMetaBuilder().withName("groupWithUser2").build(), + List.of("user2")); + + return new Object[][] { + {"user1", Collections.emptyList(), "", "", "", "", true}, + {"user1", Collections.emptyList(), "user1", "", "", "", true}, + {"user1", Collections.emptyList(), "user1", "", "user2", "", true}, + {"user1", List.of(groupWithUser2), "user1", "", "", "groupWithUser2", true}, + {"user1", List.of(groupWithUser1), "", "groupWithUser1", "", "", true}, + {"user2", List.of(groupWithUser1), "user2", "groupWithUser1", "", "", true}, + { + "user1", + List.of(groupWithUser1, groupWithUser2), + "", + "groupWithUser1", + "", + "groupWithUser2", + true + }, + {"user1", Collections.emptyList(), "user1", "", "user1", "", false}, + {"user2", Collections.emptyList(), "user1", "", "", "", false}, + {"user2", Collections.emptyList(), "user1", "", "user2", "", false}, + {"user2", List.of(groupWithUser1), "", "groupWithUser1", "", "", false}, + {"user1", Collections.emptyList(), "", "", "user1", "", false}, + {"user1", List.of(groupWithUser1), "", "", "", "groupWithUser1", false}, + {"user1", List.of(groupWithUser1), "", "groupWithUser1", "", "groupWithUser1", false}, + { + "user2", + List.of(groupWithUser1, groupWithUser2), + "", + "groupWithUser1", + "", + "groupWithUser2", + false + }, + }; + } +} diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java index 93f2d36876e..c620aab5d8e 100644 --- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java +++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/project/OpenShiftProjectFactoryTest.java @@ -71,6 +71,8 @@ import org.eclipse.che.inject.ConfigurationException; import org.eclipse.che.workspace.infrastructure.kubernetes.CheServerKubernetesClientFactory; import org.eclipse.che.workspace.infrastructure.kubernetes.api.shared.KubernetesNamespaceMeta; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.AuthorizationChecker; +import org.eclipse.che.workspace.infrastructure.kubernetes.authorization.PermissionsCleaner; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesConfigsMaps; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.KubernetesSecrets; import org.eclipse.che.workspace.infrastructure.kubernetes.namespace.configurator.NamespaceConfigurator; @@ -113,6 +115,8 @@ public class OpenShiftProjectFactoryTest { @Mock private WorkspaceManager workspaceManager; @Mock private PreferenceManager preferenceManager; @Mock private KubernetesSharedPool pool; + @Mock private AuthorizationChecker authorizationChecker; + @Mock private PermissionsCleaner permissionsCleaner; @Mock private ProjectOperation projectOperation; @@ -134,6 +138,7 @@ public void setUp() throws Exception { lenient().when(cheServerOpenshiftClientFactory.createOC()).thenReturn(osClient); lenient().when(cheServerKubernetesClientFactory.create()).thenReturn(osClient); lenient().when(osClient.projects()).thenReturn(projectOperation); + lenient().when(authorizationChecker.isAuthorized(anyString())).thenReturn(true); lenient() .when(workspaceManager.getWorkspace(any())) @@ -174,6 +179,8 @@ public void shouldNotThrowExceptionIfDefaultNamespaceIsSpecifiedOnCheckingIfName cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER); projectFactory.checkIfNamespaceIsAllowed(USER_NAME + "-che"); @@ -204,6 +211,8 @@ public void shouldNotThrowExceptionIfDefaultNamespaceIsSpecifiedOnCheckingIfName cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER); try { projectFactory.checkIfNamespaceIsAllowed("any-namespace"); @@ -234,6 +243,8 @@ public void shouldNotThrowExceptionIfDefaultNamespaceIsSpecifiedOnCheckingIfName cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER); } @@ -269,6 +280,8 @@ public void shouldReturnPreparedNamespacesWhenFound() throws InfrastructureExcep cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER); EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "123", null, false)); @@ -305,6 +318,8 @@ public void shouldNotThrowAnExceptionWhenNotAllowedToListNamespaces() throws Exc cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER); EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "u123", null, false)); @@ -337,6 +352,8 @@ public void throwAnExceptionWhenErrorListingNamespaces() throws Exception { cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER); // when @@ -379,6 +396,8 @@ public void shouldReturnDefaultProjectWhenItExistsAndUserDefinedIsNotAllowed() t cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER); List availableNamespaces = projectFactory.list(); @@ -415,6 +434,8 @@ public void shouldReturnDefaultProjectWhenItDoesNotExistAndUserDefinedIsNotAllow cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER); List availableNamespaces = projectFactory.list(); @@ -451,6 +472,8 @@ public void shouldThrowExceptionWhenFailedToGetInfoAboutDefaultNamespace() throw cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER); projectFactory.list(); @@ -477,6 +500,8 @@ public void shouldThrowExceptionWhenFailedToGetNamespaces() throws Exception { cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER); projectFactory.list(); @@ -508,6 +533,8 @@ public void shouldRequireNamespacePriorExistenceIfDifferentFromDefaultAndUserDef cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); prepareProject(toReturnProject); @@ -542,6 +569,8 @@ public void shouldCreatePreferencesConfigmapIfNotExists() throws Exception { cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); doReturn(toReturnProject).when(projectFactory).doCreateProjectAccess(any(), any()); @@ -584,6 +613,8 @@ public void shouldNotCreatePreferencesConfigmapIfExist() throws Exception { cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); prepareProject(toReturnProject); @@ -628,6 +659,8 @@ public void shouldCallStopWorkspaceRoleProvisionWhenIdentityProviderIsDefined() cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, OAUTH_IDENTITY_PROVIDER)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); when(toReturnProject.getName()).thenReturn("workspace123"); @@ -677,6 +710,8 @@ public void testEvalNamespaceNameWhenPreparedNamespacesFound() throws Infrastruc cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER); String namespace = @@ -709,6 +744,8 @@ public void testUsernamePlaceholderInLabelsIsNotEvaluated() throws Infrastructur cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER); EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "123", null, false)); projectFactory.list(); @@ -735,6 +772,8 @@ public void testUsernamePlaceholderInAnnotationsIsEvaluated() throws Infrastruct cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER)); EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "123", null, false)); OpenShiftProject toReturnProject = mock(OpenShiftProject.class); @@ -775,6 +814,8 @@ public void testAllConfiguratorsAreCalledWhenCreatingProject() throws Infrastruc cheServerOpenshiftClientFactory, preferenceManager, pool, + authorizationChecker, + permissionsCleaner, NO_OAUTH_IDENTITY_PROVIDER)); EnvironmentContext.getCurrent().setSubject(new SubjectImpl("jondoe", "123", null, false));