Skip to content

Commit

Permalink
feat: Advanced authorization (#619)
Browse files Browse the repository at this point in the history
* feat: Advanced authorization

Signed-off-by: Anatolii Bazko <[email protected]>

* Renames fields

Signed-off-by: Anatolii Bazko <[email protected]>

* Address remarks

Signed-off-by: Anatolii Bazko <[email protected]>

* Address remarks

Signed-off-by: Anatolii Bazko <[email protected]>

* Address remarks

Signed-off-by: Anatolii Bazko <[email protected]>

---------

Signed-off-by: Anatolii Bazko <[email protected]>
  • Loading branch information
tolusha authored Dec 4, 2023
1 parent 7acf4cc commit 7fe2a4d
Show file tree
Hide file tree
Showing 16 changed files with 660 additions and 40 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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/
Expand All @@ -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 {

Expand Down Expand Up @@ -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<String> strToSet(String str) {
if (!isNullOrEmpty(str)) {
return Sets.newHashSet(Splitter.on(",").trimResults().omitEmptyStrings().split(str));
} else {
return Collections.emptySet();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, InternalEnvironmentFactory> factories =
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String> allowUsers;
private final Set<String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String> 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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -98,6 +101,8 @@ public class KubernetesNamespaceFactory {
private final PreferenceManager preferenceManager;
protected final Set<NamespaceConfigurator> namespaceConfigurators;
protected final KubernetesSharedPool sharedPool;
protected final AuthorizationChecker authorizationChecker;
protected final PermissionsCleaner permissionsCleaner;

@Inject
public KubernetesNamespaceFactory(
Expand All @@ -110,7 +115,9 @@ public KubernetesNamespaceFactory(
Set<NamespaceConfigurator> namespaceConfigurators,
CheServerKubernetesClientFactory cheServerKubernetesClientFactory,
PreferenceManager preferenceManager,
KubernetesSharedPool sharedPool)
KubernetesSharedPool sharedPool,
AuthorizationChecker authorizationChecker,
PermissionsCleaner permissionsCleaner)
throws ConfigurationException {
this.namespaceCreationAllowed = namespaceCreationAllowed;
this.cheServerKubernetesClientFactory = cheServerKubernetesClientFactory;
Expand All @@ -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("=");
Expand Down Expand Up @@ -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<String, String> namespaceAnnotationsEvaluated =
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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},
};
}
}
Loading

0 comments on commit 7fe2a4d

Please sign in to comment.