diff --git a/assembly/assembly-wsmaster-war/pom.xml b/assembly/assembly-wsmaster-war/pom.xml
index a9581b18f1b..d2321995223 100644
--- a/assembly/assembly-wsmaster-war/pom.xml
+++ b/assembly/assembly-wsmaster-war/pom.xml
@@ -35,6 +35,10 @@
ch.qos.logbacklogback-classic
+
+ com.auth0
+ jwks-rsa
+ com.google.guavaguava
@@ -63,6 +67,10 @@
io.jaegertracingjaeger-tracerresolver
+
+ io.jsonwebtoken
+ jjwt-api
+ io.jsonwebtokenjjwt-impl
@@ -227,6 +235,82 @@
org.eclipse.che.infrastructureinfrastructure-openshift
+
+ org.eclipse.che.infrastructure
+ infrastructure-permission
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-authentication-commons
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-authorization
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-authorization-impl
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-permission
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-workspace-activity
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-keycloak-server
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-keycloak-token-provider
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-machine-authentication
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-oidc
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-permission-devfile
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-permission-logger
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-permission-resource
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-permission-system
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-permission-user
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-permission-workspace
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-permission-workspace-activity
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-personal-account
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-sql-schema
+ org.eclipse.persistenceorg.eclipse.persistence.core
diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java
index e4db0f6f1e0..3ff7929101c 100644
--- a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java
+++ b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterModule.java
@@ -13,13 +13,17 @@
import static com.google.inject.matcher.Matchers.subclassesOf;
import static org.eclipse.che.inject.Matchers.names;
+import static org.eclipse.che.multiuser.api.permission.server.SystemDomain.SYSTEM_DOMAIN_ACTIONS;
+import com.auth0.jwk.JwkProvider;
import com.google.inject.AbstractModule;
import com.google.inject.TypeLiteral;
import com.google.inject.assistedinject.FactoryModuleBuilder;
import com.google.inject.multibindings.MapBinder;
import com.google.inject.multibindings.Multibinder;
import com.google.inject.name.Names;
+import io.jsonwebtoken.JwtParser;
+import io.jsonwebtoken.SigningKeyResolver;
import java.util.HashMap;
import java.util.Map;
import org.eclipse.che.api.core.notification.RemoteSubscriptionStorage;
@@ -63,7 +67,6 @@
import org.eclipse.che.api.workspace.server.WorkspaceStatusCache;
import org.eclipse.che.api.workspace.server.devfile.DevfileModule;
import org.eclipse.che.api.workspace.server.hc.ServersCheckerFactory;
-import org.eclipse.che.api.workspace.server.jpa.WorkspaceJpaModule;
import org.eclipse.che.api.workspace.server.spi.provision.InternalEnvironmentProvisioner;
import org.eclipse.che.api.workspace.server.spi.provision.MachineNameProvisioner;
import org.eclipse.che.api.workspace.server.spi.provision.env.AgentAuthEnableEnvVarProvider;
@@ -79,11 +82,22 @@
import org.eclipse.che.api.workspace.server.spi.provision.env.WorkspaceIdEnvVarProvider;
import org.eclipse.che.api.workspace.server.spi.provision.env.WorkspaceNameEnvVarProvider;
import org.eclipse.che.api.workspace.server.spi.provision.env.WorkspaceNamespaceNameEnvVarProvider;
-import org.eclipse.che.api.workspace.server.token.MachineTokenProvider;
import org.eclipse.che.api.workspace.server.wsplugins.ChePluginsApplier;
import org.eclipse.che.commons.observability.deploy.ExecutorWrapperModule;
import org.eclipse.che.core.tracing.metrics.TracingMetricsModule;
import org.eclipse.che.inject.DynaModule;
+import org.eclipse.che.multiuser.api.authentication.commons.token.HeaderRequestTokenExtractor;
+import org.eclipse.che.multiuser.api.authentication.commons.token.RequestTokenExtractor;
+import org.eclipse.che.multiuser.api.permission.server.PermissionChecker;
+import org.eclipse.che.multiuser.api.permission.server.PermissionCheckerImpl;
+import org.eclipse.che.multiuser.api.workspace.activity.MultiUserWorkspaceActivityModule;
+import org.eclipse.che.multiuser.machine.authentication.server.MachineAuthModule;
+import org.eclipse.che.multiuser.oidc.OIDCInfo;
+import org.eclipse.che.multiuser.oidc.OIDCInfoProvider;
+import org.eclipse.che.multiuser.oidc.OIDCJwkProvider;
+import org.eclipse.che.multiuser.oidc.OIDCJwtParserProvider;
+import org.eclipse.che.multiuser.oidc.OIDCSigningKeyResolver;
+import org.eclipse.che.multiuser.permission.user.UserServicePermissionsFilter;
import org.eclipse.che.security.PBKDF2PasswordEncryptor;
import org.eclipse.che.security.PasswordEncryptor;
import org.eclipse.che.security.oauth.EmbeddedOAuthAPI;
@@ -94,7 +108,6 @@
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesInfrastructure;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
import org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth.KubernetesOidcProviderConfigFactory;
-import org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth.RequestTokenExtractor;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.SecureServerExposer;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.SecureServerExposerFactory;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.PassThroughProxySecureServerExposer;
@@ -107,7 +120,7 @@
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftInfraModule;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftInfrastructure;
import org.eclipse.che.workspace.infrastructure.openshift.environment.OpenShiftEnvironment;
-import org.eclipse.che.workspace.infrastructure.openshift.multiuser.oauth.HeaderRequestTokenExtractor;
+import org.eclipse.che.workspace.infrastructure.openshift.multiuser.oauth.KeycloakProviderConfigFactory;
import org.eclipse.persistence.config.PersistenceUnitProperties;
/** @author andrew00x */
@@ -311,6 +324,9 @@ private void configureMultiUserMode(
if (OpenShiftInfrastructure.NAME.equals(infrastructure)
|| KubernetesInfrastructure.NAME.equals(infrastructure)) {
install(new ReplicationModule(persistenceProperties));
+ bind(
+ org.eclipse.che.multiuser.permission.workspace.infra.kubernetes
+ .BrokerServicePermissionFilter.class);
configureJwtProxySecureProvisioner(infrastructure);
} else {
bind(RemoteSubscriptionStorage.class)
@@ -321,24 +337,70 @@ private void configureMultiUserMode(
.to(org.eclipse.che.api.workspace.server.DefaultWorkspaceStatusCache.class);
}
- bind(KubernetesClientConfigFactory.class).to(KubernetesOidcProviderConfigFactory.class);
+ if (Boolean.parseBoolean(System.getenv("CHE_AUTH_NATIVEUSER"))) {
+ bind(KubernetesClientConfigFactory.class).to(KubernetesOidcProviderConfigFactory.class);
+ } else if (OpenShiftInfrastructure.NAME.equals(infrastructure)) {
+ bind(KubernetesClientConfigFactory.class).to(KeycloakProviderConfigFactory.class);
+ }
persistenceProperties.put(
PersistenceUnitProperties.EXCEPTION_HANDLER_CLASS,
"org.eclipse.che.core.db.postgresql.jpa.eclipselink.PostgreSqlExceptionHandler");
- bind(RequestTokenExtractor.class).to(HeaderRequestTokenExtractor.class);
- bind(ProfileDao.class).to(JpaProfileDao.class);
- bind(OAuthAPI.class).to(EmbeddedOAuthAPI.class).asEagerSingleton();
+ install(
+ new org.eclipse.che.multiuser.permission.workspace.server.WorkspaceApiPermissionsModule());
+ install(
+ new org.eclipse.che.multiuser.permission.workspace.server.jpa
+ .MultiuserWorkspaceJpaModule());
+ install(new MultiUserWorkspaceActivityModule());
+ install(
+ new org.eclipse.che.multiuser.permission.devfile.server.jpa
+ .MultiuserUserDevfileJpaModule());
+ install(
+ new org.eclipse.che.multiuser.permission.devfile.server.UserDevfileApiPermissionsModule());
+
+ // Permission filters
+ bind(org.eclipse.che.multiuser.permission.system.SystemServicePermissionsFilter.class);
+ bind(org.eclipse.che.multiuser.permission.system.JvmServicePermissionsFilter.class);
+ bind(
+ org.eclipse.che.multiuser.permission.system.SystemEventsSubscriptionPermissionsCheck.class);
+
+ Multibinder binder =
+ Multibinder.newSetBinder(binder(), String.class, Names.named(SYSTEM_DOMAIN_ACTIONS));
+ binder.addBinding().toInstance(UserServicePermissionsFilter.MANAGE_USERS_ACTION);
+ bind(org.eclipse.che.multiuser.permission.user.UserProfileServicePermissionsFilter.class);
+ bind(org.eclipse.che.multiuser.permission.user.UserServicePermissionsFilter.class);
+ bind(org.eclipse.che.multiuser.permission.logger.LoggerServicePermissionsFilter.class);
+
+ bind(org.eclipse.che.multiuser.permission.workspace.activity.ActivityPermissionsFilter.class);
+
+ bind(
+ org.eclipse.che.multiuser.permission.resource.filters.ResourceServicePermissionsFilter
+ .class);
+ bind(
+ org.eclipse.che.multiuser.permission.resource.filters
+ .FreeResourcesLimitServicePermissionsFilter.class);
+
+ if (Boolean.parseBoolean(System.getenv("CHE_AUTH_NATIVEUSER"))) {
+ bind(RequestTokenExtractor.class).to(HeaderRequestTokenExtractor.class);
+ if (KubernetesInfrastructure.NAME.equals(infrastructure)) {
+ bind(OIDCInfo.class).toProvider(OIDCInfoProvider.class).asEagerSingleton();
+ bind(SigningKeyResolver.class).to(OIDCSigningKeyResolver.class);
+ bind(JwtParser.class).toProvider(OIDCJwtParserProvider.class);
+ bind(JwkProvider.class).toProvider(OIDCJwkProvider.class);
+ }
+ bind(TokenValidator.class).to(NotImplementedTokenValidator.class);
+ bind(ProfileDao.class).to(JpaProfileDao.class);
+ bind(OAuthAPI.class).to(EmbeddedOAuthAPI.class).asEagerSingleton();
+ }
- install(new WorkspaceJpaModule());
- bind(TokenValidator.class).to(NotImplementedTokenValidator.class);
- bind(MachineTokenProvider.class).to(MachineTokenProvider.EmptyMachineTokenProvider.class);
+ install(new MachineAuthModule());
// User and profile - use profile from keycloak and other stuff is JPA
bind(PasswordEncryptor.class).to(PBKDF2PasswordEncryptor.class);
bind(UserDao.class).to(JpaUserDao.class);
bind(PreferenceDao.class).to(JpaPreferenceDao.class);
+ bind(PermissionChecker.class).to(PermissionCheckerImpl.class);
bindConstant().annotatedWith(Names.named("che.agents.auth_enabled")).to(true);
}
diff --git a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterServletModule.java b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterServletModule.java
index 5d6ac3f1217..92817290f14 100644
--- a/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterServletModule.java
+++ b/assembly/assembly-wsmaster-war/src/main/java/org/eclipse/che/api/deploy/WsMasterServletModule.java
@@ -17,8 +17,10 @@
import org.eclipse.che.commons.logback.filter.RequestIdLoggerFilter;
import org.eclipse.che.inject.ConfigurationException;
import org.eclipse.che.inject.DynaModule;
+import org.eclipse.che.multiuser.keycloak.server.deploy.KeycloakServletModule;
+import org.eclipse.che.multiuser.machine.authentication.server.MachineLoginFilter;
+import org.eclipse.che.multiuser.oidc.filter.OidcTokenInitializationFilter;
import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesInfrastructure;
-import org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth.OidcTokenInitializationFilter;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftInfrastructure;
import org.eclipse.che.workspace.infrastructure.openshift.multiuser.oauth.OpenshiftTokenInitializationFilter;
import org.everrest.guice.servlet.GuiceEverrestServlet;
@@ -45,7 +47,14 @@ protected void configureServlets() {
// Matching group SHOULD contain forward slash.
serveRegex("^(?!/websocket.?)(.*)")
.with(GuiceEverrestServlet.class, ImmutableMap.of("openapi.context.id", "org.eclipse.che"));
- configureNativeUserMode();
+
+ if (Boolean.parseBoolean(System.getenv("CHE_AUTH_NATIVEUSER"))) {
+ LOG.info("Running in native-user mode ...");
+ configureNativeUserMode();
+ } else {
+ LOG.info("Running in classic multi-user mode ...");
+ configureMultiUserMode();
+ }
if (Boolean.valueOf(System.getenv("CHE_METRICS_ENABLED"))) {
install(new org.eclipse.che.core.metrics.MetricsServletModule());
@@ -62,6 +71,11 @@ private boolean isCheCorsEnabled() {
}
}
+ private void configureMultiUserMode() {
+ filterRegex(".*").through(MachineLoginFilter.class);
+ install(new KeycloakServletModule());
+ }
+
private void configureNativeUserMode() {
final String infrastructure = System.getenv("CHE_INFRASTRUCTURE_ACTIVE");
if (OpenShiftInfrastructure.NAME.equals(infrastructure)) {
diff --git a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/web.xml b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/web.xml
index a99b6dc80b5..bb0025e18f3 100644
--- a/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/web.xml
+++ b/assembly/assembly-wsmaster-war/src/main/webapp/WEB-INF/web.xml
@@ -36,4 +36,8 @@
javax.sql.DataSource
+
+ org.eclipse.che.multiuser.api.authentication.commons.DestroySessionListener
+
+
diff --git a/infrastructures/infrastructure-permission/pom.xml b/infrastructures/infrastructure-permission/pom.xml
new file mode 100644
index 00000000000..a922b06a4af
--- /dev/null
+++ b/infrastructures/infrastructure-permission/pom.xml
@@ -0,0 +1,75 @@
+
+
+
+ 4.0.0
+
+ che-infrastructures-parent
+ org.eclipse.che.infrastructure
+ 7.103.0-SNAPSHOT
+
+ infrastructure-permission
+ Infrastructure :: Kubernetes Permissions
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ org.eclipse.che.core
+ che-core-api-core
+
+
+ org.eclipse.che.core
+ che-core-api-workspace-shared
+
+
+ org.eclipse.che.infrastructure
+ infrastructure-kubernetes
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-permission
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-permission-workspace
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+ org.eclipse.che.core
+ che-core-api-dto
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-testng
+ test
+
+
+ org.testng
+ testng
+ test
+
+
+
diff --git a/infrastructures/infrastructure-permission/src/main/java/org/eclipse/che/multiuser/permission/workspace/infra/kubernetes/BrokerServicePermissionFilter.java b/infrastructures/infrastructure-permission/src/main/java/org/eclipse/che/multiuser/permission/workspace/infra/kubernetes/BrokerServicePermissionFilter.java
new file mode 100644
index 00000000000..0251ae2277c
--- /dev/null
+++ b/infrastructures/infrastructure-permission/src/main/java/org/eclipse/che/multiuser/permission/workspace/infra/kubernetes/BrokerServicePermissionFilter.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.permission.workspace.infra.kubernetes;
+
+import static org.eclipse.che.workspace.infrastructure.kubernetes.wsplugins.events.BrokerService.BROKER_RESULT_METHOD;
+import static org.eclipse.che.workspace.infrastructure.kubernetes.wsplugins.events.BrokerService.BROKER_STATUS_CHANGED_METHOD;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.jsonrpc.commons.RequestHandlerManager;
+import org.eclipse.che.api.workspace.shared.dto.RuntimeIdentityDto;
+import org.eclipse.che.api.workspace.shared.dto.event.BrokerStatusChangedEvent;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.multiuser.api.permission.server.jsonrpc.JsonRpcPermissionsFilterAdapter;
+import org.eclipse.che.multiuser.permission.workspace.server.WorkspaceDomain;
+import org.eclipse.che.workspace.infrastructure.kubernetes.wsplugins.events.BrokerService;
+
+/**
+ * Add permissions checking before {@link BrokerService} methods invocation.
+ *
+ * @author Sergii Leshchenko
+ */
+@Singleton
+public class BrokerServicePermissionFilter extends JsonRpcPermissionsFilterAdapter {
+ @Inject
+ public void register(RequestHandlerManager requestHandlerManager) {
+ requestHandlerManager.registerMethodInvokerFilter(
+ this, BROKER_STATUS_CHANGED_METHOD, BROKER_RESULT_METHOD);
+ }
+
+ @Override
+ public void doAccept(String method, Object... params) throws ForbiddenException {
+ String workspaceId;
+ switch (method) {
+ case BROKER_STATUS_CHANGED_METHOD:
+ case BROKER_RESULT_METHOD:
+ RuntimeIdentityDto runtimeId = ((BrokerStatusChangedEvent) params[0]).getRuntimeId();
+ if (runtimeId == null || runtimeId.getWorkspaceId() == null) {
+ throw new ForbiddenException("Workspace id must be specified");
+ }
+ workspaceId = runtimeId.getWorkspaceId();
+ break;
+ default:
+ throw new ForbiddenException("Unknown method is configured to be filtered.");
+ }
+
+ Subject currentSubject = EnvironmentContext.getCurrent().getSubject();
+ if (!currentSubject.hasPermission(
+ WorkspaceDomain.DOMAIN_ID, workspaceId, WorkspaceDomain.RUN)) {
+ throw new ForbiddenException(
+ "User doesn't have the required permissions to the specified workspace");
+ }
+ }
+}
diff --git a/infrastructures/infrastructure-permission/src/test/java/org/eclipse/che/multiuser/permission/workspace/infra/kubernetes/BrokerServicePermissionFilterTest.java b/infrastructures/infrastructure-permission/src/test/java/org/eclipse/che/multiuser/permission/workspace/infra/kubernetes/BrokerServicePermissionFilterTest.java
new file mode 100644
index 00000000000..c2aede4190a
--- /dev/null
+++ b/infrastructures/infrastructure-permission/src/test/java/org/eclipse/che/multiuser/permission/workspace/infra/kubernetes/BrokerServicePermissionFilterTest.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.permission.workspace.infra.kubernetes;
+
+import static org.eclipse.che.workspace.infrastructure.kubernetes.wsplugins.events.BrokerService.BROKER_RESULT_METHOD;
+import static org.eclipse.che.workspace.infrastructure.kubernetes.wsplugins.events.BrokerService.BROKER_STATUS_CHANGED_METHOD;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.jsonrpc.commons.RequestHandlerManager;
+import org.eclipse.che.api.workspace.shared.dto.RuntimeIdentityDto;
+import org.eclipse.che.api.workspace.shared.dto.event.BrokerStatusChangedEvent;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.permission.workspace.server.WorkspaceDomain;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests {@link BrokerServicePermissionFilter}
+ *
+ * @author Sergii Leshchenko
+ */
+@Listeners(MockitoTestNGListener.class)
+public class BrokerServicePermissionFilterTest {
+
+ @Mock private RequestHandlerManager requestHandlerManager;
+
+ @Mock private Subject subject;
+
+ private BrokerServicePermissionFilter permissionFilter;
+
+ @BeforeMethod
+ public void setUp() {
+ EnvironmentContext.getCurrent().setSubject(subject);
+ permissionFilter = new BrokerServicePermissionFilter();
+ }
+
+ @AfterMethod
+ public void tearDown() {
+ EnvironmentContext.reset();
+ }
+
+ @Test
+ public void shouldRegisterItself() {
+ // when
+ permissionFilter.register(requestHandlerManager);
+
+ // then
+ requestHandlerManager.registerMethodInvokerFilter(
+ permissionFilter, BROKER_STATUS_CHANGED_METHOD, BROKER_RESULT_METHOD);
+ }
+
+ @Test(
+ dataProvider = "coveredMethods",
+ expectedExceptions = ForbiddenException.class,
+ expectedExceptionsMessageRegExp =
+ "User doesn't have the required permissions to the specified workspace")
+ public void shouldThrowExceptionIfUserDoesNotHaveRunPermission(String method) throws Exception {
+ // given
+ when(subject.hasPermission(eq(WorkspaceDomain.DOMAIN_ID), eq("ws123"), eq(WorkspaceDomain.RUN)))
+ .thenReturn(false);
+
+ // when
+ permissionFilter.doAccept(
+ method,
+ DtoFactory.newDto(BrokerStatusChangedEvent.class)
+ .withRuntimeId(DtoFactory.newDto(RuntimeIdentityDto.class).withWorkspaceId("ws123")));
+ }
+
+ @Test(dataProvider = "coveredMethods")
+ public void shouldDoNothingIfUserHasRunPermissions(String method) throws Exception {
+ // given
+ when(subject.hasPermission(WorkspaceDomain.DOMAIN_ID, "ws123", WorkspaceDomain.RUN))
+ .thenReturn(true);
+
+ // when
+ permissionFilter.doAccept(
+ method,
+ DtoFactory.newDto(BrokerStatusChangedEvent.class)
+ .withRuntimeId(DtoFactory.newDto(RuntimeIdentityDto.class).withWorkspaceId("ws123")));
+ }
+
+ @Test(
+ expectedExceptions = ForbiddenException.class,
+ expectedExceptionsMessageRegExp = "Unknown method is configured to be filtered\\.")
+ public void shouldThrowExceptionIfUnknownMethodIsInvoking() throws Exception {
+ // when
+ permissionFilter.doAccept(
+ "unknown",
+ DtoFactory.newDto(BrokerStatusChangedEvent.class)
+ .withRuntimeId(DtoFactory.newDto(RuntimeIdentityDto.class).withWorkspaceId("ws123")));
+ }
+
+ @DataProvider
+ public Object[][] coveredMethods() {
+ return new Object[][] {{BROKER_STATUS_CHANGED_METHOD}, {BROKER_RESULT_METHOD}};
+ }
+}
diff --git a/infrastructures/infrastructure-permission/src/test/resources/logback-test.xml b/infrastructures/infrastructure-permission/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..c7a7b0b5cf3
--- /dev/null
+++ b/infrastructures/infrastructure-permission/src/test/resources/logback-test.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n%nopex
+
+
+
+ target/log/test.log
+
+ %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n
+
+
+
+
+
+
+
+
+
+
diff --git a/infrastructures/kubernetes/pom.xml b/infrastructures/kubernetes/pom.xml
index 69d748de606..1b5e92ad116 100644
--- a/infrastructures/kubernetes/pom.xml
+++ b/infrastructures/kubernetes/pom.xml
@@ -89,10 +89,6 @@
io.fabric8openshift-model
-
- io.jsonwebtoken
- jjwt-api
- io.opentracingopentracing-api
@@ -109,10 +105,6 @@
jakarta.injectjakarta.inject-api
-
- jakarta.servlet
- jakarta.servlet-api
- jakarta.validationjakarta.validation-api
@@ -185,6 +177,10 @@
org.eclipse.che.coreche-core-commons-tracing
+
+ org.eclipse.che.multiuser
+ che-multiuser-machine-authentication
+ org.eclipse.persistencejakarta.persistence
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/AbstractJwtProxyProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/AbstractJwtProxyProvisioner.java
index 2ce89c27c5c..8eb56cc322f 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/AbstractJwtProxyProvisioner.java
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/AbstractJwtProxyProvisioner.java
@@ -11,6 +11,7 @@
*/
package org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy;
+import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static org.eclipse.che.api.core.model.workspace.config.MachineConfig.CPU_LIMIT_ATTRIBUTE;
import static org.eclipse.che.api.core.model.workspace.config.MachineConfig.CPU_REQUEST_ATTRIBUTE;
@@ -19,19 +20,25 @@
import static org.eclipse.che.api.workspace.shared.Constants.CONTAINER_SOURCE_ATTRIBUTE;
import static org.eclipse.che.api.workspace.shared.Constants.TOOL_CONTAINER_SOURCE;
import static org.eclipse.che.commons.lang.NameGenerator.generate;
+import static org.eclipse.che.workspace.infrastructure.kubernetes.Constants.CHE_ORIGINAL_NAME_LABEL;
import static org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerExposer.SERVER_PREFIX;
import static org.eclipse.che.workspace.infrastructure.kubernetes.server.KubernetesServerExposer.SERVER_UNIQUE_PART_SIZE;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
+import io.fabric8.kubernetes.api.model.ConfigMap;
+import io.fabric8.kubernetes.api.model.ConfigMapBuilder;
import io.fabric8.kubernetes.api.model.ContainerBuilder;
import io.fabric8.kubernetes.api.model.Pod;
import io.fabric8.kubernetes.api.model.PodBuilder;
+import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServicePort;
import io.fabric8.kubernetes.api.model.ServicePortBuilder;
import io.fabric8.kubernetes.api.model.VolumeBuilder;
import io.fabric8.kubernetes.api.model.VolumeMount;
+import java.security.KeyPair;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -43,6 +50,7 @@
import org.eclipse.che.workspace.infrastructure.kubernetes.Names;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodData;
+import org.eclipse.che.workspace.infrastructure.kubernetes.server.ServerServiceBuilder;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServiceExposureStrategy;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.ProxyProvisioner;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.factory.JwtProxyConfigBuilderFactory;
@@ -70,11 +78,13 @@ abstract class AbstractJwtProxyProvisioner implements ProxyProvisioner {
private final CookiePathStrategy cookiePathStrategy;
private final MultiHostCookiePathStrategy multihostCookiePathStrategy;
private int availablePort;
+ private final KeyPair keyPair;
private final boolean detectCookieAuth;
/**
* Constructor!
*
+ * @param signatureKeyPair the key pair for JWT proxy SSH comms
* @param jwtProxyConfigBuilderFactory factory to create a JWT proxy config builder
* @param externalServiceExposureStrategy the strategy to expose external servers
* @param cookiePathStrategy the strategy for the cookie path of the JWT auth cookies, if used
@@ -85,6 +95,7 @@ abstract class AbstractJwtProxyProvisioner implements ProxyProvisioner {
* whether to ignore such requirements
*/
AbstractJwtProxyProvisioner(
+ KeyPair signatureKeyPair,
JwtProxyConfigBuilderFactory jwtProxyConfigBuilderFactory,
ExternalServiceExposureStrategy externalServiceExposureStrategy,
ExternalServiceExposureStrategy multiHostStrategy,
@@ -97,6 +108,7 @@ abstract class AbstractJwtProxyProvisioner implements ProxyProvisioner {
String cpuLimitCores,
String workspaceId,
boolean detectCookieAuth) {
+ this.keyPair = signatureKeyPair;
this.proxyConfigBuilder = jwtProxyConfigBuilderFactory.create(workspaceId);
this.jwtProxyImage = jwtProxyImage;
this.externalServiceExposureStrategy = externalServiceExposureStrategy;
@@ -158,6 +170,7 @@ public ServicePort expose(
throws InfrastructureException {
Preconditions.checkArgument(
secureServers != null && !secureServers.isEmpty(), "Secure servers are missing");
+ ensureJwtProxyInjected(k8sEnv, machineName, pod);
Set excludes = new HashSet<>();
Boolean cookiesAuthEnabled = null;
@@ -238,6 +251,44 @@ String getConfigMapName() {
return "jwtproxy-config";
}
+ private void ensureJwtProxyInjected(KubernetesEnvironment k8sEnv, String machineName, PodData pod)
+ throws InfrastructureException {
+ if (!k8sEnv.getMachines().containsKey(JWT_PROXY_MACHINE_NAME)) {
+ k8sEnv.getMachines().put(JWT_PROXY_MACHINE_NAME, createJwtProxyMachine());
+ Pod jwtProxyPod = createJwtProxyPod();
+ k8sEnv.addInjectablePod(machineName, JWT_PROXY_MACHINE_NAME, jwtProxyPod);
+
+ Map initConfigMapData = new HashMap<>();
+ initConfigMapData.put(
+ JWT_PROXY_PUBLIC_KEY_FILE,
+ PUBLIC_KEY_HEADER
+ + java.util.Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded())
+ + PUBLIC_KEY_FOOTER);
+
+ initConfigMapData.put(JWT_PROXY_CONFIG_FILE, proxyConfigBuilder.build());
+
+ ConfigMap jwtProxyConfigMap =
+ new ConfigMapBuilder()
+ .withNewMetadata()
+ .withName(getConfigMapName())
+ .endMetadata()
+ .withData(initConfigMapData)
+ .build();
+ k8sEnv.getConfigMaps().put(jwtProxyConfigMap.getMetadata().getName(), jwtProxyConfigMap);
+
+ Service jwtProxyService =
+ new ServerServiceBuilder()
+ .withName(serviceName)
+ // we're merely injecting the pod, so we need a selector that is going to hit the
+ // pod that runs the server that we're exposing
+ .withSelectorEntry(CHE_ORIGINAL_NAME_LABEL, pod.getMetadata().getName())
+ .withMachineName(JWT_PROXY_MACHINE_NAME)
+ .withPorts(emptyList())
+ .build();
+ k8sEnv.getServices().put(jwtProxyService.getMetadata().getName(), jwtProxyService);
+ }
+ }
+
private InternalMachineConfig createJwtProxyMachine() {
return new InternalMachineConfig(emptyMap(), emptyMap(), attributes, null);
}
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisioner.java
index b3d65be7f82..3aa79032e8e 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisioner.java
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisioner.java
@@ -12,10 +12,14 @@
package org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy;
import com.google.inject.assistedinject.Assisted;
+import java.security.KeyPair;
import javax.inject.Inject;
import javax.inject.Named;
import org.eclipse.che.api.core.model.workspace.config.ServerConfig;
import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
+import org.eclipse.che.api.workspace.server.spi.InternalInfrastructureException;
+import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager;
+import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManagerException;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ServiceExposureStrategyProvider;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.factory.JwtProxyConfigBuilderFactory;
@@ -36,12 +40,14 @@
*
*
* @see JwtProxyConfigBuilder
+ * @see SignatureKeyManager
* @author Sergii Leshchenko
*/
public class JwtProxyProvisioner extends AbstractJwtProxyProvisioner {
@Inject
public JwtProxyProvisioner(
+ SignatureKeyManager signatureKeyManager,
JwtProxyConfigBuilderFactory jwtProxyConfigBuilderFactory,
ServiceExposureStrategyProvider serviceExposureStrategyProvider,
CookiePathStrategy cookiePathStrategy,
@@ -51,8 +57,10 @@ public JwtProxyProvisioner(
@Named("che.server.secure_exposer.jwtproxy.memory_limit") String memoryLimitBytes,
@Named("che.server.secure_exposer.jwtproxy.cpu_request") String cpuRequestCores,
@Named("che.server.secure_exposer.jwtproxy.cpu_limit") String cpuLimitCores,
- @Assisted RuntimeIdentity identity) {
+ @Assisted RuntimeIdentity identity)
+ throws InternalInfrastructureException {
super(
+ constructKeyPair(signatureKeyManager, identity),
jwtProxyConfigBuilderFactory,
serviceExposureStrategyProvider.get(),
serviceExposureStrategyProvider.getMultiHostStrategy(),
@@ -67,6 +75,18 @@ public JwtProxyProvisioner(
true);
}
+ private static KeyPair constructKeyPair(
+ SignatureKeyManager signatureKeyManager, RuntimeIdentity identity)
+ throws InternalInfrastructureException {
+ try {
+ return signatureKeyManager.getOrCreateKeyPair(identity.getWorkspaceId());
+ } catch (SignatureKeyManagerException e) {
+ throw new InternalInfrastructureException(
+ "Signature key pair for machine authentication cannot be retrieved. Reason: "
+ + e.getMessage());
+ }
+ }
+
@Override
protected ExposureConfiguration getExposureConfiguration(ServerConfig serverConfig) {
return new ExposureConfiguration(serverConfig);
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/PassThroughProxyProvisioner.java b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/PassThroughProxyProvisioner.java
index e47f18d3586..12ae688329f 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/PassThroughProxyProvisioner.java
+++ b/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/PassThroughProxyProvisioner.java
@@ -43,8 +43,10 @@ public PassThroughProxyProvisioner(
@Named("che.server.secure_exposer.jwtproxy.memory_limit") String memoryLimitBytes,
@Named("che.server.secure_exposer.jwtproxy.cpu_request") String cpuRequestCores,
@Named("che.server.secure_exposer.jwtproxy.cpu_limit") String cpuLimitCores,
- @Assisted RuntimeIdentity identity) {
+ @Assisted RuntimeIdentity identity)
+ throws InternalInfrastructureException {
super(
+ constructSignatureKeyPair(),
jwtProxyConfigBuilderFactory,
serviceExposureStrategyProvider.get(),
serviceExposureStrategyProvider.getMultiHostStrategy(),
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisionerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisionerTest.java
index 4c8ae2d4f8b..461205a0972 100644
--- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisionerTest.java
+++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/JwtProxyProvisionerTest.java
@@ -23,6 +23,7 @@
import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxyProvisioner.PUBLIC_KEY_FOOTER;
import static org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.JwtProxyProvisioner.PUBLIC_KEY_HEADER;
import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.lenient;
@@ -45,6 +46,7 @@
import io.fabric8.kubernetes.api.model.Service;
import io.fabric8.kubernetes.api.model.ServicePort;
import java.net.URI;
+import java.security.KeyPair;
import java.security.PublicKey;
import java.util.Base64;
import java.util.regex.Pattern;
@@ -53,6 +55,7 @@
import org.eclipse.che.api.workspace.server.model.impl.ServerConfigImpl;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.api.workspace.server.spi.environment.InternalMachineConfig;
+import org.eclipse.che.multiuser.machine.authentication.server.signature.SignatureKeyManager;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment;
import org.eclipse.che.workspace.infrastructure.kubernetes.environment.KubernetesEnvironment.PodData;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ExternalServiceExposureStrategy;
@@ -61,7 +64,6 @@
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
import org.testng.annotations.BeforeMethod;
-import org.testng.annotations.Ignore;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@@ -71,7 +73,6 @@
* @author Sergii Leshchenko
*/
@Listeners(MockitoTestNGListener.class)
-@Ignore
public class JwtProxyProvisionerTest {
private static final String WORKSPACE_ID = "workspace123";
@@ -80,6 +81,7 @@ public class JwtProxyProvisionerTest {
private final RuntimeIdentity runtimeId =
new RuntimeIdentityImpl(WORKSPACE_ID, "env123", "owner123", "infraNamespace");
+ @Mock private SignatureKeyManager signatureKeyManager;
@Mock private PublicKey publicKey;
@Mock private JwtProxyConfigBuilderFactory configBuilderFactory;
@Mock private ServiceExposureStrategyProvider serviceExposureStrategyProvider;
@@ -94,6 +96,8 @@ public class JwtProxyProvisionerTest {
@BeforeMethod
public void setUp() throws Exception {
+ when(signatureKeyManager.getOrCreateKeyPair(anyString()))
+ .thenReturn(new KeyPair(publicKey, null));
lenient().when(publicKey.getEncoded()).thenReturn("publickey".getBytes());
when(configBuilderFactory.create(any()))
@@ -107,6 +111,7 @@ public void setUp() throws Exception {
jwtProxyProvisioner =
new JwtProxyProvisioner(
+ signatureKeyManager,
configBuilderFactory,
serviceExposureStrategyProvider,
cookiePathStrategy,
@@ -228,6 +233,7 @@ public void shouldUseCookiesAuthEnabledFromServersConfigs() throws Exception {
jwtProxyProvisioner =
new JwtProxyProvisioner(
+ signatureKeyManager,
configBuilderFactory,
serviceExposureStrategyProvider,
cookiePathStrategy,
@@ -278,6 +284,7 @@ public void shouldFalseValueAsDefaultForCookiesAuthEnabledAttribute() throws Exc
jwtProxyProvisioner =
new JwtProxyProvisioner(
+ signatureKeyManager,
configBuilderFactory,
serviceExposureStrategyProvider,
cookiePathStrategy,
@@ -312,7 +319,6 @@ public void shouldFalseValueAsDefaultForCookiesAuthEnabledAttribute() throws Exc
}
@Test
- @Ignore
public void shouldBindToLocalhostWhenNoServiceForServerExists() throws Exception {
// given
JwtProxyConfigBuilder configBuilder = mock(JwtProxyConfigBuilder.class);
@@ -320,6 +326,7 @@ public void shouldBindToLocalhostWhenNoServiceForServerExists() throws Exception
jwtProxyProvisioner =
new JwtProxyProvisioner(
+ signatureKeyManager,
configBuilderFactory,
serviceExposureStrategyProvider,
cookiePathStrategy,
@@ -354,7 +361,6 @@ public void shouldBindToLocalhostWhenNoServiceForServerExists() throws Exception
}
@Test
- @Ignore
public void multiHostStrategiesUsedForServerRequiringSubdomain() throws Exception {
// given
JwtProxyConfigBuilder configBuilder = mock(JwtProxyConfigBuilder.class);
@@ -362,6 +368,7 @@ public void multiHostStrategiesUsedForServerRequiringSubdomain() throws Exceptio
jwtProxyProvisioner =
new JwtProxyProvisioner(
+ signatureKeyManager,
configBuilderFactory,
serviceExposureStrategyProvider,
cookiePathStrategy,
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/PassThroughProxyProvisionerTest.java b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/PassThroughProxyProvisionerTest.java
index f86d8a13ae7..60ddcc48782 100644
--- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/PassThroughProxyProvisionerTest.java
+++ b/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/server/secure/jwtproxy/PassThroughProxyProvisionerTest.java
@@ -36,7 +36,6 @@
import org.eclipse.che.workspace.infrastructure.kubernetes.server.external.ServiceExposureStrategyProvider;
import org.eclipse.che.workspace.infrastructure.kubernetes.server.secure.jwtproxy.factory.JwtProxyConfigBuilderFactory;
import org.mockito.testng.MockitoTestNGListener;
-import org.testng.annotations.Ignore;
import org.testng.annotations.Listeners;
import org.testng.annotations.Test;
@@ -51,7 +50,6 @@ public class PassThroughProxyProvisionerTest {
// test the different behavior here, while the majority of the tests are present in the
// JwtProxyProvisionerTest
- @Ignore
@Test
public void shouldConfigureProxyWithExcludes() throws Exception {
// given
diff --git a/infrastructures/openshift/pom.xml b/infrastructures/openshift/pom.xml
index 4969a19f5ed..3b5ff9cc307 100644
--- a/infrastructures/openshift/pom.xml
+++ b/infrastructures/openshift/pom.xml
@@ -124,6 +124,26 @@
org.eclipse.che.infrastructureinfrastructure-kubernetes
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-authentication-commons
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-authorization
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-keycloak-server
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-keycloak-shared
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-oidc
+ org.slf4jslf4j-api
diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactory.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactory.java
new file mode 100644
index 00000000000..b0db0299304
--- /dev/null
+++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactory.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.oauth;
+
+import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.CLIENT_ID_SETTING;
+import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;
+import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.AUTH_SERVER_URL_SETTING;
+
+import com.google.inject.Provider;
+import io.fabric8.kubernetes.client.Config;
+import io.fabric8.openshift.client.OpenShiftConfig;
+import io.fabric8.openshift.client.OpenShiftConfigBuilder;
+import jakarta.ws.rs.core.UriBuilder;
+import java.io.UnsupportedEncodingException;
+import java.net.URI;
+import java.net.URLEncoder;
+import java.util.Optional;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.UnauthorizedException;
+import org.eclipse.che.api.workspace.server.WorkspaceRuntimes;
+import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
+import org.eclipse.che.api.workspace.server.spi.RuntimeContext;
+import org.eclipse.che.commons.annotation.Nullable;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.multiuser.keycloak.server.KeycloakServiceClient;
+import org.eclipse.che.multiuser.keycloak.server.KeycloakSettings;
+import org.eclipse.che.multiuser.keycloak.shared.dto.KeycloakTokenResponse;
+import org.eclipse.che.workspace.infrastructure.kubernetes.KubernetesClientConfigFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This class retrieves the OpenShift OAuth token of the current Che user, and injects it the
+ * OpenShift {@link Config} object, so that workspace OpenShift resources will be created under the
+ * OpenShift account of the current Che user.
+ *
+ *
The OpenShift OAuth token is retrieved using the OpenShift identity provider configured in the
+ * Keycloak server.
+ *
+ *
If the current user is not the user that started the current workspace (for operations such as
+ * idling, che server shutdown, etc ...), then global OpenShift infrastructure credentials defined
+ * in the Che properties will be used.
+ *
+ * @author David Festal
+ */
+@Singleton
+public class KeycloakProviderConfigFactory extends KubernetesClientConfigFactory {
+
+ private static final Logger LOG = LoggerFactory.getLogger(KeycloakProviderConfigFactory.class);
+
+ private final String oauthIdentityProvider;
+
+ private final KeycloakServiceClient keycloakServiceClient;
+ private final Provider workspaceRuntimeProvider;
+ private final String messageToLinkAccount;
+
+ @Inject
+ public KeycloakProviderConfigFactory(
+ KeycloakServiceClient keycloakServiceClient,
+ KeycloakSettings keycloakSettings,
+ Provider workspaceRuntimeProvider,
+ @Nullable @Named("che.infra.openshift.oauth_identity_provider") String oauthIdentityProvider,
+ @Named("che.api") String apiEndpoint,
+ @Nullable @Named("che.infra.kubernetes.master_url") String masterUrl,
+ @Nullable @Named("che.infra.kubernetes.trust_certs") Boolean doTrustCerts) {
+ super(masterUrl, doTrustCerts);
+ this.keycloakServiceClient = keycloakServiceClient;
+ this.workspaceRuntimeProvider = workspaceRuntimeProvider;
+ this.oauthIdentityProvider = oauthIdentityProvider;
+
+ messageToLinkAccount =
+ "You should link your account with the "
+ + oauthIdentityProvider
+ + " \n"
+ + "identity provider by visiting the "
+ + "Federated Identities page of your Che account";
+ }
+
+ private String buildReferrerURI(String apiEndpoint) {
+ URI referrerURI =
+ UriBuilder.fromUri(apiEndpoint)
+ .replacePath("dashboard/")
+ .queryParam("redirect_fragment", "/workspaces")
+ .build();
+ try {
+ return URLEncoder.encode(referrerURI.toString(), "UTF-8");
+ } catch (UnsupportedEncodingException e) {
+ throw new RuntimeException(
+ "Error occurred during constructing Referrer URI. " + e.getMessage(), e);
+ }
+ }
+
+ @Override
+ public boolean isPersonalized() {
+ // config is personalized only if OAuth is configured and the current user is not anonymous
+ return oauthIdentityProvider != null;
+ }
+
+ /**
+ * Builds the OpenShift {@link Config} object based on a default {@link Config} object and an
+ * optional workspace Id.
+ */
+ public Config buildConfig(Config defaultConfig, @Nullable String workspaceId)
+ throws InfrastructureException {
+ Subject subject = EnvironmentContext.getCurrent().getSubject();
+
+ if (oauthIdentityProvider == null) {
+ LOG.debug("OAuth Provider is not configured, default config is used.");
+ return defaultConfig;
+ }
+
+ if (subject == Subject.ANONYMOUS) {
+ LOG.debug(
+ "OAuth Provider is configured but default subject is anonymous, default config is used.");
+ return defaultConfig;
+ }
+
+ if (workspaceId == null) {
+ LOG.debug(
+ "OAuth Provider is configured and this request is not related to any workspace. OAuth token will be retrieved.");
+ return personalizeConfig(defaultConfig);
+ }
+
+ Optional context =
+ workspaceRuntimeProvider.get().getRuntimeContext(workspaceId);
+ if (!context.isPresent()) {
+ // there is no cached info for this workspace in workspace API.
+ // it means that it's not started yet and it's initial call for preparing context
+ LOG.debug(
+ "There is no runtime context for the specified workspace '%s'. It's the first workspace "
+ + "related call, so context is personalized with OAuth token.");
+ return personalizeConfig(defaultConfig);
+ }
+ String workspaceOwnerId = context.map(c -> c.getIdentity().getOwnerId()).orElse(null);
+
+ boolean isRuntimeOwner = subject.getUserId().equals(workspaceOwnerId);
+
+ if (!isRuntimeOwner) {
+ LOG.debug(
+ "OAuth Provider is configured, but current subject is not runtime owner, default config is used."
+ + "Subject user id: '{}'. Runtime owner id: '{}'",
+ subject.getUserId(),
+ workspaceOwnerId);
+ return defaultConfig;
+ }
+
+ LOG.debug(
+ "OAuth Provider is configured and current subject is runtime owner. OAuth token will be retrieved.");
+ return personalizeConfig(defaultConfig);
+ }
+
+ private Config personalizeConfig(Config defaultConfig) throws InfrastructureException {
+ try {
+ KeycloakTokenResponse keycloakTokenInfos =
+ keycloakServiceClient.getIdentityProviderToken(oauthIdentityProvider);
+ if ("user:full".equals(keycloakTokenInfos.getScope())) {
+ return new OpenShiftConfigBuilder(OpenShiftConfig.wrap(defaultConfig))
+ .withOauthToken(keycloakTokenInfos.getAccessToken())
+ .build();
+ } else {
+ throw new InfrastructureException(
+ "Cannot retrieve user OpenShift token: '"
+ + oauthIdentityProvider
+ + "' identity provider is not granted full rights: "
+ + oauthIdentityProvider);
+ }
+ } catch (UnauthorizedException e) {
+ LOG.error("Cannot retrieve User OpenShift token from the identity provider", e);
+
+ throw new InfrastructureException(messageToLinkAccount);
+ } catch (BadRequestException e) {
+ LOG.error(
+ "Cannot retrieve User OpenShift token from the '"
+ + oauthIdentityProvider
+ + "' identity provider",
+ e);
+ if (e.getMessage().endsWith("Invalid token.")) {
+ throw new InfrastructureException(
+ "Your session has expired. \nPlease "
+ + ""
+ + "login"
+ + " to Che again to get access to your OpenShift account");
+ }
+ throw new InfrastructureException(e.getMessage(), e);
+ } catch (Exception e) {
+ LOG.error(
+ "Cannot retrieve User OpenShift token from the '"
+ + oauthIdentityProvider
+ + "' identity provider",
+ e);
+ throw new InfrastructureException(e.getMessage(), e);
+ }
+ }
+}
diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilter.java b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilter.java
index 06e7de7c90e..fb5167613e9 100644
--- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilter.java
+++ b/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilter.java
@@ -25,10 +25,11 @@
import org.eclipse.che.api.user.server.UserManager;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.commons.subject.SubjectImpl;
-import org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth.AuthorizedSubject;
-import org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth.MultiUserEnvironmentInitializationFilter;
-import org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth.RequestTokenExtractor;
-import org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth.SessionStore;
+import org.eclipse.che.multiuser.api.authentication.commons.SessionStore;
+import org.eclipse.che.multiuser.api.authentication.commons.filter.MultiUserEnvironmentInitializationFilter;
+import org.eclipse.che.multiuser.api.authentication.commons.token.RequestTokenExtractor;
+import org.eclipse.che.multiuser.api.permission.server.AuthorizedSubject;
+import org.eclipse.che.multiuser.api.permission.server.PermissionChecker;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -44,6 +45,7 @@ public class OpenshiftTokenInitializationFilter
private static final Logger LOG =
LoggerFactory.getLogger(OpenshiftTokenInitializationFilter.class);
+ private final PermissionChecker permissionChecker;
private final OpenShiftClientFactory clientFactory;
private final UserManager userManager;
@@ -53,10 +55,12 @@ public OpenshiftTokenInitializationFilter(
SessionStore sessionStore,
RequestTokenExtractor tokenExtractor,
OpenShiftClientFactory clientFactory,
- UserManager userManager) {
+ UserManager userManager,
+ PermissionChecker permissionChecker) {
super(sessionStore, tokenExtractor);
this.clientFactory = clientFactory;
this.userManager = userManager;
+ this.permissionChecker = permissionChecker;
}
@Override
@@ -88,7 +92,8 @@ protected Subject extractSubject(String token, io.fabric8.openshift.api.model.Us
try {
ObjectMeta userMeta = osu.getMetadata();
User user = userManager.getOrCreateUser(getUserId(osu), userMeta.getName());
- return new AuthorizedSubject(new SubjectImpl(user.getName(), user.getId(), token, false));
+ return new AuthorizedSubject(
+ new SubjectImpl(user.getName(), user.getId(), token, false), permissionChecker);
} catch (ServerException | ConflictException e) {
throw new RuntimeException(e);
}
diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactoryTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactoryTest.java
new file mode 100644
index 00000000000..a7bc2806705
--- /dev/null
+++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/KeycloakProviderConfigFactoryTest.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.oauth;
+
+import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.CLIENT_ID_SETTING;
+import static org.eclipse.che.multiuser.keycloak.shared.KeycloakConstants.REALM_SETTING;
+import static org.eclipse.che.multiuser.oidc.OIDCInfoProvider.AUTH_SERVER_URL_SETTING;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertSame;
+import static org.testng.Assert.fail;
+
+import com.google.inject.Provider;
+import io.fabric8.kubernetes.client.Config;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.UnauthorizedException;
+import org.eclipse.che.api.core.model.workspace.runtime.RuntimeIdentity;
+import org.eclipse.che.api.core.rest.shared.dto.ServiceError;
+import org.eclipse.che.api.workspace.server.WorkspaceRuntimes;
+import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
+import org.eclipse.che.api.workspace.server.spi.RuntimeContext;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.keycloak.server.KeycloakServiceClient;
+import org.eclipse.che.multiuser.keycloak.server.KeycloakSettings;
+import org.eclipse.che.multiuser.keycloak.shared.dto.KeycloakTokenResponse;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/** @author David Festal */
+@Listeners(MockitoTestNGListener.class)
+public class KeycloakProviderConfigFactoryTest {
+ private static final String PROVIDER = "openshift-v3";
+ private static final String THE_USER_ID = "a_user_id";
+ private static final String ANOTHER_USER_ID = "another_user_id";
+ private static final String A_WORKSPACE_ID = "workspace_id";
+ private static final String FULL_SCOPE = "user:full";
+ private static final String ACCESS_TOKEN = "accessToken";
+ private static final String AUTH_SERVER_URL = "http://keycloak.url/auth";
+ private static final String REALM = "realm";
+ private static final String CLIENT_ID = "clientId";
+ private static final String API_ENDPOINT = "http://che-host/api";
+
+ private static final String SHOULD_LINK_ERROR_MESSAGE =
+ "You should link your account with the "
+ + PROVIDER
+ + " \n"
+ + "identity provider by visiting the "
+ + "Federated Identities page of your Che account";
+
+ private static final String SESSION_EXPIRED_MESSAGE =
+ "Your session has expired. \nPlease "
+ + ""
+ + "login"
+ + " to Che again to get access to your OpenShift account";
+
+ private static final Map keycloakSettingsMap = new HashMap();
+
+ @Mock private KeycloakServiceClient keycloakServiceClient;
+ @Mock private KeycloakSettings keycloakSettings;
+ @Mock private Provider workspaceRuntimeProvider;
+ @Mock private WorkspaceRuntimes workspaceRuntimes;
+ @Mock private Subject subject;
+ @Mock private RuntimeIdentity runtimeIdentity;
+
+ @SuppressWarnings("rawtypes")
+ @Mock
+ private RuntimeContext runtimeContext;
+
+ @Mock private KeycloakTokenResponse tokenResponse;
+
+ private EnvironmentContext context;
+ private KeycloakProviderConfigFactory configBuilder;
+ private Config defaultConfig;
+
+ static {
+ keycloakSettingsMap.put(AUTH_SERVER_URL_SETTING, AUTH_SERVER_URL);
+ keycloakSettingsMap.put(REALM_SETTING, REALM);
+ keycloakSettingsMap.put(CLIENT_ID_SETTING, CLIENT_ID);
+ }
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ when(keycloakSettings.get()).thenReturn(keycloakSettingsMap);
+ context = spy(EnvironmentContext.getCurrent());
+ EnvironmentContext.setCurrent(context);
+ lenient().doReturn(subject).when(context).getSubject();
+ lenient().when(workspaceRuntimeProvider.get()).thenReturn(workspaceRuntimes);
+ lenient()
+ .when(workspaceRuntimes.getRuntimeContext(anyString()))
+ .thenReturn(Optional.ofNullable(runtimeContext));
+ lenient().when(runtimeContext.getIdentity()).thenReturn(runtimeIdentity);
+ lenient().when(runtimeIdentity.getOwnerId()).thenReturn(THE_USER_ID);
+ lenient().when(subject.getUserId()).thenReturn(THE_USER_ID);
+ lenient().when(tokenResponse.getScope()).thenReturn(FULL_SCOPE);
+ lenient().when(tokenResponse.getAccessToken()).thenReturn(ACCESS_TOKEN);
+
+ configBuilder =
+ new KeycloakProviderConfigFactory(
+ keycloakServiceClient,
+ keycloakSettings,
+ workspaceRuntimeProvider,
+ PROVIDER,
+ API_ENDPOINT,
+ null,
+ null);
+ defaultConfig = new io.fabric8.kubernetes.client.ConfigBuilder().build();
+ }
+
+ @AfterMethod
+ public void cleanup() {
+ EnvironmentContext.reset();
+ }
+
+ @Test
+ public void testFallbackToDefaultConfigWhenProvideIsNull() throws Exception {
+ configBuilder =
+ new KeycloakProviderConfigFactory(
+ keycloakServiceClient,
+ keycloakSettings,
+ workspaceRuntimeProvider,
+ null,
+ API_ENDPOINT,
+ null,
+ null);
+ assertSame(defaultConfig, configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID));
+ }
+
+ @Test
+ public void testFallbackToDefaultConfigWhenSubjectIsAnonymous() throws Exception {
+ doReturn(Subject.ANONYMOUS).when(context).getSubject();
+ assertSame(defaultConfig, configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID));
+ }
+
+ @Test
+ public void testFallbackToDefaultConfigWhenCurrentUserIsDifferentFromWorkspaceOwner()
+ throws Exception {
+ when(runtimeIdentity.getOwnerId()).thenReturn(ANOTHER_USER_ID);
+ assertSame(defaultConfig, configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID));
+ }
+
+ @SuppressWarnings("rawtypes")
+ @Test
+ public void testCreateUserConfigWhenNoRuntimeContext() throws Exception {
+ when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse);
+ when(workspaceRuntimes.getRuntimeContext(anyString())).thenReturn(Optional.empty());
+
+ Config resultConfig = configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
+ assertEquals(resultConfig.getOauthToken(), ACCESS_TOKEN);
+ }
+
+ @Test
+ public void testCreateUserConfigWhenWorkspaceIdIsNull() throws Exception {
+ when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse);
+ Config resultConfig = configBuilder.buildConfig(defaultConfig, null);
+ assertEquals(resultConfig.getOauthToken(), ACCESS_TOKEN);
+ }
+
+ @Test
+ public void testCreateUserConfig() throws Exception {
+ when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse);
+ Config resultConfig = configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
+ assertEquals(resultConfig.getOauthToken(), ACCESS_TOKEN);
+ }
+
+ @Test(expectedExceptions = {InfrastructureException.class})
+ public void testThrowOnBadScope() throws Exception {
+ when(keycloakServiceClient.getIdentityProviderToken(anyString())).thenReturn(tokenResponse);
+ when(tokenResponse.getScope()).thenReturn("bad:scope");
+ Config resultConfig = configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
+ assertEquals(resultConfig.getOauthToken(), ACCESS_TOKEN);
+ }
+
+ @Test
+ public void testRethrowOnUnauthorizedException() throws Exception {
+ doThrow(
+ new UnauthorizedException(
+ DtoFactory.newDto(ServiceError.class).withMessage("Any other message")))
+ .when(keycloakServiceClient)
+ .getIdentityProviderToken(anyString());
+ try {
+ configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
+ } catch (InfrastructureException e) {
+ assertEquals(e.getMessage(), SHOULD_LINK_ERROR_MESSAGE, "The exception message is wrong");
+ return;
+ }
+ fail(
+ "Should have thrown an exception with the following message: " + SHOULD_LINK_ERROR_MESSAGE);
+ }
+
+ @Test(expectedExceptions = {InfrastructureException.class})
+ public void testRethrowOnBadRequestException() throws Exception {
+ doThrow(
+ new BadRequestException(
+ DtoFactory.newDto(ServiceError.class).withMessage("Any other message")))
+ .when(keycloakServiceClient)
+ .getIdentityProviderToken(anyString());
+ configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
+ }
+
+ @Test
+ public void testRethrowOnInvalidTokenBadRequestException() throws Exception {
+ doThrow(
+ new BadRequestException(
+ DtoFactory.newDto(ServiceError.class).withMessage("Invalid token.")))
+ .when(keycloakServiceClient)
+ .getIdentityProviderToken(anyString());
+ try {
+ configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
+ } catch (InfrastructureException e) {
+ assertEquals(e.getMessage(), SESSION_EXPIRED_MESSAGE, "The exception message is wrong");
+ return;
+ }
+ fail("Should have thrown an exception with the following message: " + SESSION_EXPIRED_MESSAGE);
+ }
+
+ @Test(expectedExceptions = {InfrastructureException.class})
+ public void testRethrowOnAnyException() throws Exception {
+ when(keycloakServiceClient.getIdentityProviderToken(anyString()))
+ .thenThrow(org.eclipse.che.api.core.NotFoundException.class);
+ configBuilder.buildConfig(defaultConfig, A_WORKSPACE_ID);
+ }
+}
diff --git a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilterTest.java b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilterTest.java
index 69c1a7c4a3e..e12c37f502a 100644
--- a/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilterTest.java
+++ b/infrastructures/openshift/src/test/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/OpenshiftTokenInitializationFilterTest.java
@@ -26,8 +26,9 @@
import org.eclipse.che.api.user.server.model.impl.UserImpl;
import org.eclipse.che.api.workspace.server.spi.InfrastructureException;
import org.eclipse.che.commons.subject.Subject;
-import org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth.RequestTokenExtractor;
-import org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth.SessionStore;
+import org.eclipse.che.multiuser.api.authentication.commons.SessionStore;
+import org.eclipse.che.multiuser.api.authentication.commons.token.RequestTokenExtractor;
+import org.eclipse.che.multiuser.api.permission.server.PermissionChecker;
import org.eclipse.che.workspace.infrastructure.openshift.OpenShiftClientFactory;
import org.mockito.Mock;
import org.mockito.testng.MockitoTestNGListener;
@@ -40,6 +41,7 @@ public class OpenshiftTokenInitializationFilterTest {
@Mock private SessionStore sessionStore;
@Mock private RequestTokenExtractor tokenExtractor;
@Mock private UserManager userManager;
+ @Mock private PermissionChecker permissionChecker;
@Mock private OpenShiftClientFactory openShiftClientFactory;
@Mock private OpenShiftClient openShiftClient;
@@ -56,7 +58,7 @@ public class OpenshiftTokenInitializationFilterTest {
public void setUp() throws InfrastructureException {
openshiftTokenInitializationFilter =
new OpenshiftTokenInitializationFilter(
- sessionStore, tokenExtractor, openShiftClientFactory, userManager);
+ sessionStore, tokenExtractor, openShiftClientFactory, userManager, permissionChecker);
}
@Test
diff --git a/infrastructures/pom.xml b/infrastructures/pom.xml
index 85470e6fa6c..f9cc1da6378 100644
--- a/infrastructures/pom.xml
+++ b/infrastructures/pom.xml
@@ -29,6 +29,7 @@
kubernetesinfrastructure-metricsinfrastructure-distributed
+ infrastructure-permissioninfrastructure-factory
diff --git a/multiuser/api/che-multiuser-api-authentication-commons/pom.xml b/multiuser/api/che-multiuser-api-authentication-commons/pom.xml
new file mode 100644
index 00000000000..f5dc3e36024
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authentication-commons/pom.xml
@@ -0,0 +1,66 @@
+
+
+
+ 4.0.0
+
+ che-multiuser-api
+ org.eclipse.che.multiuser
+ 7.103.0-SNAPSHOT
+
+ che-multiuser-api-authentication-commons
+ jar
+ Che Multiuser :: API :: Authentication Commons
+
+
+ com.google.inject
+ guice
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ jakarta.servlet
+ jakarta.servlet-api
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+
+
+ org.eclipse.che.core
+ che-core-api-core
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-testng
+ test
+
+
+ org.testng
+ testng
+ test
+
+
+
diff --git a/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/Constants.java b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/Constants.java
new file mode 100644
index 00000000000..b061fc21e8a
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/Constants.java
@@ -0,0 +1,19 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.authentication.commons;
+
+/** Auth-related constants. */
+public class Constants {
+
+ /** Name of the subject attribute in the Http session */
+ public static final String CHE_SUBJECT_ATTRIBUTE = "che_subject";
+}
diff --git a/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/DestroySessionListener.java b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/DestroySessionListener.java
new file mode 100644
index 00000000000..5dfc6311ed4
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/DestroySessionListener.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.authentication.commons;
+
+import static org.eclipse.che.multiuser.api.authentication.commons.Constants.CHE_SUBJECT_ATTRIBUTE;
+
+import com.google.inject.Injector;
+import jakarta.servlet.ServletContext;
+import jakarta.servlet.http.HttpSessionEvent;
+import jakarta.servlet.http.HttpSessionListener;
+import java.util.Optional;
+import org.eclipse.che.commons.subject.Subject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Purges deleted sessions from sessions cache store. */
+public class DestroySessionListener implements HttpSessionListener {
+
+ private static final Logger LOG = LoggerFactory.getLogger(DestroySessionListener.class);
+
+ @Override
+ public final void sessionCreated(HttpSessionEvent sessionEvent) {}
+
+ @Override
+ public void sessionDestroyed(HttpSessionEvent sessionEvent) {
+
+ ServletContext servletContext = sessionEvent.getSession().getServletContext();
+
+ Optional sessionStoreOptional = getSessionStoreInstance(servletContext);
+ if (!sessionStoreOptional.isPresent()) {
+ LOG.error(
+ "Unable to remove session from store. Session store is not configured in servlet context.");
+ return;
+ }
+ SessionStore sessionStore = sessionStoreOptional.get();
+ Subject subject = (Subject) sessionEvent.getSession().getAttribute(CHE_SUBJECT_ATTRIBUTE);
+ if (subject != null) {
+ sessionStore.remove(subject.getUserId());
+ }
+ }
+
+ /** Searches session store component in servlet context when with help of guice injector. */
+ private Optional getSessionStoreInstance(ServletContext servletContext) {
+ String attributeName = SessionStore.class.getName();
+ SessionStore result = (SessionStore) servletContext.getAttribute(attributeName);
+ if (result == null) {
+ Injector injector = (Injector) servletContext.getAttribute(Injector.class.getName());
+ if (injector != null) {
+ result = injector.getInstance(SessionStore.class);
+ if (result != null) {
+ servletContext.setAttribute(attributeName, result);
+ }
+ }
+ }
+ return Optional.ofNullable(result);
+ }
+}
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/SessionStore.java b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/SessionStore.java
similarity index 95%
rename from infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/SessionStore.java
rename to multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/SessionStore.java
index 81e9d5c1254..da665359228 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/SessionStore.java
+++ b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/SessionStore.java
@@ -9,7 +9,7 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
-package org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth;
+package org.eclipse.che.multiuser.api.authentication.commons;
import jakarta.servlet.http.HttpSession;
import java.util.concurrent.ConcurrentHashMap;
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/SubjectHttpRequestWrapper.java b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/SubjectHttpRequestWrapper.java
similarity index 94%
rename from infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/SubjectHttpRequestWrapper.java
rename to multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/SubjectHttpRequestWrapper.java
index c2b24f887ce..44a4dfed141 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/SubjectHttpRequestWrapper.java
+++ b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/SubjectHttpRequestWrapper.java
@@ -9,7 +9,7 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
-package org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth;
+package org.eclipse.che.multiuser.api.authentication.commons;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/MultiUserEnvironmentInitializationFilter.java b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilter.java
similarity index 95%
rename from infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/MultiUserEnvironmentInitializationFilter.java
rename to multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilter.java
index 37675dd446e..dedea780865 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/MultiUserEnvironmentInitializationFilter.java
+++ b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilter.java
@@ -9,7 +9,9 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
-package org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth;
+package org.eclipse.che.multiuser.api.authentication.commons.filter;
+
+import static org.eclipse.che.multiuser.api.authentication.commons.Constants.CHE_SUBJECT_ATTRIBUTE;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
@@ -26,6 +28,9 @@
import java.util.Optional;
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.multiuser.api.authentication.commons.SessionStore;
+import org.eclipse.che.multiuser.api.authentication.commons.SubjectHttpRequestWrapper;
+import org.eclipse.che.multiuser.api.authentication.commons.token.RequestTokenExtractor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -47,7 +52,6 @@
* @author Max Shaposhnyk (mshaposh@redhat.com)
*/
public abstract class MultiUserEnvironmentInitializationFilter implements Filter {
- public static final String CHE_SUBJECT_ATTRIBUTE = "che_subject";
private static final Logger LOG =
LoggerFactory.getLogger(MultiUserEnvironmentInitializationFilter.class);
diff --git a/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/token/ChainedTokenExtractor.java b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/token/ChainedTokenExtractor.java
new file mode 100644
index 00000000000..ba4f10afcc7
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/token/ChainedTokenExtractor.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.authentication.commons.token;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+/**
+ * Try to extract token from request in 3 steps. 1. From query parameter. 2. From header. 3. From
+ * cookie.
+ *
+ * @author Sergii Kabashniuk
+ */
+public class ChainedTokenExtractor implements RequestTokenExtractor {
+
+ private final HeaderRequestTokenExtractor headerRequestTokenExtractor;
+
+ private final QueryRequestTokenExtractor queryRequestTokenExtractor;
+
+ public ChainedTokenExtractor() {
+ headerRequestTokenExtractor = new HeaderRequestTokenExtractor();
+ queryRequestTokenExtractor = new QueryRequestTokenExtractor();
+ }
+
+ @Override
+ public String getToken(HttpServletRequest req) {
+ String token;
+ if ((token = queryRequestTokenExtractor.getToken(req)) == null) {
+ token = headerRequestTokenExtractor.getToken(req);
+ }
+ return token;
+ }
+}
diff --git a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/HeaderRequestTokenExtractor.java b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/token/HeaderRequestTokenExtractor.java
similarity index 86%
rename from infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/HeaderRequestTokenExtractor.java
rename to multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/token/HeaderRequestTokenExtractor.java
index 70339b20794..d0c569272cf 100644
--- a/infrastructures/openshift/src/main/java/org/eclipse/che/workspace/infrastructure/openshift/multiuser/oauth/HeaderRequestTokenExtractor.java
+++ b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/token/HeaderRequestTokenExtractor.java
@@ -9,12 +9,11 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
-package org.eclipse.che.workspace.infrastructure.openshift.multiuser.oauth;
+package org.eclipse.che.multiuser.api.authentication.commons.token;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.core.HttpHeaders;
-import org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth.RequestTokenExtractor;
/** Extract sso token from request headers. */
public class HeaderRequestTokenExtractor implements RequestTokenExtractor {
diff --git a/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/token/QueryRequestTokenExtractor.java b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/token/QueryRequestTokenExtractor.java
new file mode 100644
index 00000000000..1c10baaa791
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/token/QueryRequestTokenExtractor.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.authentication.commons.token;
+
+import jakarta.servlet.http.HttpServletRequest;
+
+/** @author Max Shaposhnik (mshaposh@redhat.com) */
+public class QueryRequestTokenExtractor implements RequestTokenExtractor {
+ @Override
+ public String getToken(HttpServletRequest req) {
+ String query = req.getQueryString();
+ if (query != null) {
+ int start = query.indexOf("&token=");
+ if (start != -1 || query.startsWith("token=")) {
+ int end = query.indexOf('&', start + 7);
+ if (end == -1) {
+ end = query.length();
+ }
+ if (end != start + 7) {
+ return query.substring(start + 7, end);
+ }
+ }
+ }
+ return null;
+ }
+}
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/RequestTokenExtractor.java b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/token/RequestTokenExtractor.java
similarity index 89%
rename from infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/RequestTokenExtractor.java
rename to multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/token/RequestTokenExtractor.java
index e48d7ad7c03..f63ad4950cb 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/RequestTokenExtractor.java
+++ b/multiuser/api/che-multiuser-api-authentication-commons/src/main/java/org/eclipse/che/multiuser/api/authentication/commons/token/RequestTokenExtractor.java
@@ -9,7 +9,7 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
-package org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth;
+package org.eclipse.che.multiuser.api.authentication.commons.token;
import jakarta.servlet.http.HttpServletRequest;
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/SessionStoreTest.java b/multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/SessionStoreTest.java
similarity index 95%
rename from infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/SessionStoreTest.java
rename to multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/SessionStoreTest.java
index 1925c8fec58..4c63e85a16a 100644
--- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/SessionStoreTest.java
+++ b/multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/SessionStoreTest.java
@@ -9,7 +9,7 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
-package org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth;
+package org.eclipse.che.multiuser.api.authentication.commons;
import static org.mockito.Mockito.mock;
import static org.testng.Assert.assertEquals;
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/SubjectHttpRequestWrapperTest.java b/multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/SubjectHttpRequestWrapperTest.java
similarity index 96%
rename from infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/SubjectHttpRequestWrapperTest.java
rename to multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/SubjectHttpRequestWrapperTest.java
index 13b563ed1f8..916c94af07e 100644
--- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/SubjectHttpRequestWrapperTest.java
+++ b/multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/SubjectHttpRequestWrapperTest.java
@@ -9,7 +9,7 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
-package org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth;
+package org.eclipse.che.multiuser.api.authentication.commons;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
diff --git a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/MultiUserEnvironmentInitializationFilterTest.java b/multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilterTest.java
similarity index 95%
rename from infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/MultiUserEnvironmentInitializationFilterTest.java
rename to multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilterTest.java
index b7bde59eaa4..92002de28f3 100644
--- a/infrastructures/kubernetes/src/test/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/MultiUserEnvironmentInitializationFilterTest.java
+++ b/multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/filter/MultiUserEnvironmentInitializationFilterTest.java
@@ -9,9 +9,9 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
-package org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth;
+package org.eclipse.che.multiuser.api.authentication.commons.filter;
-import static org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth.MultiUserEnvironmentInitializationFilter.CHE_SUBJECT_ATTRIBUTE;
+import static org.eclipse.che.multiuser.api.authentication.commons.Constants.CHE_SUBJECT_ATTRIBUTE;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
@@ -34,6 +34,8 @@
import org.eclipse.che.commons.env.EnvironmentContext;
import org.eclipse.che.commons.subject.Subject;
import org.eclipse.che.commons.subject.SubjectImpl;
+import org.eclipse.che.multiuser.api.authentication.commons.SessionStore;
+import org.eclipse.che.multiuser.api.authentication.commons.token.RequestTokenExtractor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.testng.MockitoTestNGListener;
diff --git a/multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/token/HeaderRequestTokenExtractorTest.java b/multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/token/HeaderRequestTokenExtractorTest.java
new file mode 100644
index 00000000000..85d19114cff
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authentication-commons/src/test/java/org/eclipse/che/multiuser/api/authentication/commons/token/HeaderRequestTokenExtractorTest.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.authentication.commons.token;
+
+import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.ws.rs.BadRequestException;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+@Listeners(MockitoTestNGListener.class)
+public class HeaderRequestTokenExtractorTest {
+
+ private HeaderRequestTokenExtractor tokenExtractor = new HeaderRequestTokenExtractor();
+
+ @Mock HttpServletRequest servletRequest;
+
+ @Test(dataProvider = "validHeadersProvider")
+ public void shouldExtractTokensFromValidHeaders(String headerValue, String expectedToken) {
+
+ when(servletRequest.getHeader(eq(AUTHORIZATION))).thenReturn(headerValue);
+
+ // when
+ String token = tokenExtractor.getToken(servletRequest);
+
+ // then
+ assertEquals(token, expectedToken);
+ }
+
+ @Test(
+ dataProvider = "invalidHeadersProvider",
+ expectedExceptions = BadRequestException.class,
+ expectedExceptionsMessageRegExp = "Invalid authorization header format.")
+ public void shouldThrowExceptionOnInvalidToken(String headerValue) {
+
+ when(servletRequest.getHeader(eq(AUTHORIZATION))).thenReturn(headerValue);
+
+ // when
+ tokenExtractor.getToken(servletRequest);
+ }
+
+ @DataProvider
+ private Object[][] validHeadersProvider() {
+ return new Object[][] {
+ {"token123", "token123"},
+ {"bearer token123", "token123"},
+ {"Bearer token123", "token123"},
+ };
+ }
+
+ @DataProvider
+ private Object[][] invalidHeadersProvider() {
+ return new Object[][] {{"bearertoken123"}, {"bearer token123"}, {"bearer token 123"}};
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-authorization-impl/pom.xml b/multiuser/api/che-multiuser-api-authorization-impl/pom.xml
new file mode 100644
index 00000000000..b2be908f102
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authorization-impl/pom.xml
@@ -0,0 +1,78 @@
+
+
+
+ 4.0.0
+
+ che-multiuser-api
+ org.eclipse.che.multiuser
+ 7.103.0-SNAPSHOT
+
+ che-multiuser-api-authorization-impl
+ jar
+ Che Multiuser :: API :: Authorization Impl
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ org.eclipse.che.core
+ che-core-api-core
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-authorization
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-permission
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+ test
+
+
+ org.eclipse.che.core
+ che-core-api-dto
+ test
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-permission-shared
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-testng
+ test
+
+
+ org.testng
+ testng
+ test
+
+
+
diff --git a/multiuser/api/che-multiuser-api-authorization-impl/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionCheckerImpl.java b/multiuser/api/che-multiuser-api-authorization-impl/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionCheckerImpl.java
new file mode 100644
index 00000000000..a7bf708e3fe
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authorization-impl/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionCheckerImpl.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server;
+
+import javax.inject.Inject;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+
+/**
+ * Implementation of {@link PermissionChecker} that use {@link PermissionsManager} for checking.
+ *
+ * @author Sergii Leschenko
+ */
+public class PermissionCheckerImpl implements PermissionChecker {
+ private final PermissionsManager permissionsManager;
+
+ @Inject
+ public PermissionCheckerImpl(PermissionsManager permissionsManager) {
+ this.permissionsManager = permissionsManager;
+ }
+
+ @Override
+ public boolean hasPermission(String user, String domain, String instance, String action)
+ throws ServerException, NotFoundException, ConflictException {
+ return permissionsManager.exists(user, domain, instance, action)
+ || permissionsManager.exists("*", domain, instance, action);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-authorization-impl/src/test/java/org/eclipse/che/multiuser/api/permission/server/HttpPermissionCheckerImplTest.java b/multiuser/api/che-multiuser-api-authorization-impl/src/test/java/org/eclipse/che/multiuser/api/permission/server/HttpPermissionCheckerImplTest.java
new file mode 100644
index 00000000000..ef6c4f1b615
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authorization-impl/src/test/java/org/eclipse/che/multiuser/api/permission/server/HttpPermissionCheckerImplTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server;
+
+import static java.util.Arrays.asList;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.RETURNS_DEFAULTS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+import jakarta.ws.rs.core.UriBuilder;
+import org.eclipse.che.api.core.rest.HttpJsonRequest;
+import org.eclipse.che.api.core.rest.HttpJsonRequestFactory;
+import org.eclipse.che.api.core.rest.HttpJsonResponse;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.api.permission.shared.dto.PermissionsDto;
+import org.mockito.Mock;
+import org.mockito.stubbing.Answer;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link HttpPermissionCheckerImpl}.
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners(MockitoTestNGListener.class)
+public class HttpPermissionCheckerImplTest {
+ private static final String API_ENDPOINT = "http://localhost:8000/api";
+
+ @Mock private HttpJsonRequestFactory requestFactory;
+ @Mock private HttpJsonResponse response;
+ private HttpJsonRequest request;
+
+ private HttpPermissionCheckerImpl httpPermissionChecker;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ request =
+ mock(
+ HttpJsonRequest.class,
+ (Answer)
+ invocation -> {
+ if (invocation.getMethod().getReturnType().isInstance(invocation.getMock())) {
+ return invocation.getMock();
+ }
+ return RETURNS_DEFAULTS.answer(invocation);
+ });
+ when(request.request()).thenReturn(response);
+ when(requestFactory.fromUrl(anyString())).thenReturn(request);
+
+ httpPermissionChecker = new HttpPermissionCheckerImpl(API_ENDPOINT, requestFactory);
+ }
+
+ @Test
+ public void shouldCheckPermissionsByHttpRequestToPermissionsService() throws Exception {
+ when(response.asDto(anyObject()))
+ .thenReturn(
+ DtoFactory.newDto(PermissionsDto.class)
+ .withUserId("user123")
+ .withDomainId("domain123")
+ .withInstanceId("instance123")
+ .withActions(asList("read", "test")));
+
+ final boolean hasPermission =
+ httpPermissionChecker.hasPermission("user123", "domain123", "instance123", "test");
+
+ assertEquals(hasPermission, true);
+ verify(requestFactory)
+ .fromUrl(
+ eq(
+ UriBuilder.fromUri(API_ENDPOINT)
+ .path(PermissionsService.class)
+ .path(PermissionsService.class, "getCurrentUsersPermissions")
+ .queryParam("instance", "instance123")
+ .build("domain123")
+ .toString()));
+ verify(request).useGetMethod();
+ verify(request).request();
+ verifyNoMoreInteractions(request);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-authorization-impl/src/test/java/org/eclipse/che/multiuser/api/permission/server/PermissionCheckerImplTest.java b/multiuser/api/che-multiuser-api-authorization-impl/src/test/java/org/eclipse/che/multiuser/api/permission/server/PermissionCheckerImplTest.java
new file mode 100644
index 00000000000..353182a5b18
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authorization-impl/src/test/java/org/eclipse/che/multiuser/api/permission/server/PermissionCheckerImplTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+@Listeners(MockitoTestNGListener.class)
+public class PermissionCheckerImplTest {
+ @Mock private PermissionsManager permissionsManager;
+
+ @InjectMocks private PermissionCheckerImpl permissionChecker;
+
+ @Test
+ public void shouldCheckExistingDirectUsersPermissions() throws Exception {
+ when(permissionsManager.exists(anyString(), anyString(), anyString(), anyString()))
+ .thenReturn(true);
+
+ boolean hasPermission =
+ permissionChecker.hasPermission("user123", "domain123", "instance123", "test");
+
+ assertEquals(hasPermission, true);
+ verify(permissionsManager).exists("user123", "domain123", "instance123", "test");
+ }
+
+ @Test
+ public void shouldCheckExistingPublicPermissionsIfThereIsNoDirectUsersPermissions()
+ throws Exception {
+ doReturn(false)
+ .when(permissionsManager)
+ .exists(eq("user123"), anyString(), anyString(), anyString());
+ doReturn(true).when(permissionsManager).exists(eq("*"), anyString(), anyString(), anyString());
+
+ boolean hasPermission =
+ permissionChecker.hasPermission("user123", "domain123", "instance123", "test");
+
+ assertEquals(hasPermission, true);
+ verify(permissionsManager).exists("user123", "domain123", "instance123", "test");
+ verify(permissionsManager).exists("*", "domain123", "instance123", "test");
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-authorization-impl/src/test/resources/logback-test.xml b/multiuser/api/che-multiuser-api-authorization-impl/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..ca64276c7d2
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authorization-impl/src/test/resources/logback-test.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n
+
+
+
+
+ target/log/authorization-impl.log
+
+ %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/multiuser/api/che-multiuser-api-authorization/pom.xml b/multiuser/api/che-multiuser-api-authorization/pom.xml
new file mode 100644
index 00000000000..b7d9ece730e
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authorization/pom.xml
@@ -0,0 +1,175 @@
+
+
+
+ 4.0.0
+
+ che-multiuser-api
+ org.eclipse.che.multiuser
+ 7.103.0-SNAPSHOT
+
+ che-multiuser-api-authorization
+ jar
+ Che Multiuser :: API :: Authorization
+
+ ${project.build.directory}/generated-sources/dto/
+
+
+
+ com.google.code.gson
+ gson
+
+
+ com.google.guava
+ guava
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+
+
+ org.eclipse.che.core
+ che-core-api-core
+
+
+ org.eclipse.che.core
+ che-core-api-dto
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-permission-shared
+
+
+ org.slf4j
+ slf4j-api
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-permission
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-testng
+ test
+
+
+ org.testng
+ testng
+ test
+
+
+
+
+
+ src/main/java
+
+
+ src/main/resources
+
+
+ ${dto-generator-out-directory}
+
+
+
+
+ org.eclipse.che.core
+ che-core-api-dto-maven-plugin
+ ${project.version}
+
+
+ process-sources
+
+ generate
+
+
+
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-permission-shared
+ ${project.version}
+
+
+
+
+ org.eclipse.che.api.permission.shared.dto
+
+ ${dto-generator-out-directory}
+ org.eclipse.che.multiuser.api.permission.server.dto.DtoServerImpls
+ server
+
+
+
+ maven-compiler-plugin
+
+
+ pre-compile
+ generate-sources
+
+ compile
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ add-domain
+ process-sources
+
+ add-resource
+
+
+
+
+ ${dto-generator-out-directory}/META-INF
+ META-INF
+
+
+
+
+
+ add-source
+ process-sources
+
+ add-source
+
+
+
+ ${dto-generator-out-directory}
+
+
+
+
+
+
+
+
diff --git a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/AuthorizedSubject.java b/multiuser/api/che-multiuser-api-authorization/src/main/java/org/eclipse/che/multiuser/api/permission/server/AuthorizedSubject.java
similarity index 64%
rename from infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/AuthorizedSubject.java
rename to multiuser/api/che-multiuser-api-authorization/src/main/java/org/eclipse/che/multiuser/api/permission/server/AuthorizedSubject.java
index 71fa5b21c82..609db80bc30 100644
--- a/infrastructures/kubernetes/src/main/java/org/eclipse/che/workspace/infrastructure/kubernetes/multiuser/oauth/AuthorizedSubject.java
+++ b/multiuser/api/che-multiuser-api-authorization/src/main/java/org/eclipse/che/multiuser/api/permission/server/AuthorizedSubject.java
@@ -9,9 +9,14 @@
* Contributors:
* Red Hat, Inc. - initial API and implementation
*/
-package org.eclipse.che.workspace.infrastructure.kubernetes.multiuser.oauth;
+package org.eclipse.che.multiuser.api.permission.server;
+import static java.lang.String.format;
+
+import org.eclipse.che.api.core.ConflictException;
import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
import org.eclipse.che.commons.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -26,9 +31,11 @@ public class AuthorizedSubject implements Subject {
private static final Logger LOG = LoggerFactory.getLogger(AuthorizedSubject.class);
private final Subject baseSubject;
+ private final PermissionChecker permissionChecker;
- public AuthorizedSubject(Subject baseSubject) {
+ public AuthorizedSubject(Subject baseSubject, PermissionChecker permissionChecker) {
this.baseSubject = baseSubject;
+ this.permissionChecker = permissionChecker;
}
@Override
@@ -38,7 +45,18 @@ public String getUserName() {
@Override
public boolean hasPermission(String domain, String instance, String action) {
- return true;
+ try {
+ return permissionChecker.hasPermission(getUserId(), domain, instance, action);
+ } catch (NotFoundException nfe) {
+ return false;
+ } catch (ServerException | ConflictException e) {
+ LOG.error(
+ format(
+ "Can't check permissions for user '%s' and instance '%s' of domain '%s'",
+ getUserId(), domain, instance),
+ e);
+ throw new RuntimeException("Can't check user's permissions", e);
+ }
}
@Override
diff --git a/multiuser/api/che-multiuser-api-authorization/src/main/java/org/eclipse/che/multiuser/api/permission/server/HttpPermissionCheckerImpl.java b/multiuser/api/che-multiuser-api-authorization/src/main/java/org/eclipse/che/multiuser/api/permission/server/HttpPermissionCheckerImpl.java
new file mode 100644
index 00000000000..3a4e6b8a736
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authorization/src/main/java/org/eclipse/che/multiuser/api/permission/server/HttpPermissionCheckerImpl.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server;
+
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import jakarta.ws.rs.core.UriBuilder;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.rest.HttpJsonRequestFactory;
+import org.eclipse.che.multiuser.api.permission.shared.dto.PermissionsDto;
+
+/**
+ * Implementation of {@link PermissionChecker} that load permissions by http requests to {@link
+ * PermissionsService}
+ *
+ *
It also caches permissions to avoid frequently requests to workspace master.
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class HttpPermissionCheckerImpl implements PermissionChecker {
+ private final LoadingCache> permissionsCache;
+
+ @Inject
+ public HttpPermissionCheckerImpl(
+ @Named("che.api") String apiEndpoint, HttpJsonRequestFactory requestFactory) {
+ // TODO mb make configurable size of cache and expiration time
+ this.permissionsCache =
+ CacheBuilder.newBuilder()
+ .maximumSize(1000)
+ .expireAfterWrite(1, TimeUnit.MINUTES)
+ .build(
+ new CacheLoader>() {
+ @Override
+ public Set load(Key key) throws Exception {
+ UriBuilder currentUsersPermissions =
+ UriBuilder.fromUri(apiEndpoint).path("permissions/" + key.domain);
+ if (key.instance != null) {
+ currentUsersPermissions.queryParam("instance", key.instance);
+ }
+ String userPermissionsUrl = currentUsersPermissions.build().toString();
+ try {
+ PermissionsDto usersPermissions =
+ requestFactory
+ .fromUrl(userPermissionsUrl)
+ .useGetMethod()
+ .request()
+ .asDto(PermissionsDto.class);
+ return new HashSet<>(usersPermissions.getActions());
+ } catch (NotFoundException e) {
+ // user doesn't have permissions
+ return new HashSet<>();
+ }
+ }
+ });
+ }
+
+ @Override
+ public boolean hasPermission(String user, String domain, String instance, String action)
+ throws ServerException {
+ try {
+ return permissionsCache.get(new Key(user, domain, instance)).contains(action);
+ } catch (Exception e) {
+ throw new ServerException(e.getMessage(), e);
+ }
+ }
+
+ private static final class Key {
+ private final String user;
+ private final String domain;
+ private final String instance;
+
+ private Key(String user, String domain, String instance) {
+ this.user = user;
+ this.domain = domain;
+ this.instance = instance;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof Key)) {
+ return false;
+ }
+ final Key other = (Key) obj;
+ return Objects.equals(user, other.user)
+ && Objects.equals(domain, other.domain)
+ && Objects.equals(instance, other.instance);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = hash * 31 + Objects.hashCode(user);
+ hash = hash * 31 + Objects.hashCode(domain);
+ hash = hash * 31 + Objects.hashCode(instance);
+ return hash;
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-authorization/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionChecker.java b/multiuser/api/che-multiuser-api-authorization/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionChecker.java
new file mode 100644
index 00000000000..c91857c6137
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authorization/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionChecker.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server;
+
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+
+/**
+ * Checks user's permission to perform some action with particular instance of given domain.
+ *
+ * @author Sergii Leschenko
+ */
+public interface PermissionChecker {
+ /**
+ * Checks user's permission to perform some action with particular instance.
+ *
+ * @param user user id
+ * @param domain domain id
+ * @param instance instance id
+ * @param action action name
+ * @return true if the user has given permission
+ * @throws NotFoundException when given domain is unsupported
+ * @throws ConflictException when given domain requires non nullable value for instance but it is
+ * null
+ * @throws ServerException when any other error occurs during permission existence checking
+ */
+ boolean hasPermission(String user, String domain, String instance, String action)
+ throws ServerException, NotFoundException, ConflictException;
+}
diff --git a/multiuser/api/che-multiuser-api-authorization/src/test/java/org/eclipse/che/multiuser/api/permission/server/HttpPermissionCheckerImplTest.java b/multiuser/api/che-multiuser-api-authorization/src/test/java/org/eclipse/che/multiuser/api/permission/server/HttpPermissionCheckerImplTest.java
new file mode 100644
index 00000000000..ef6c4f1b615
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authorization/src/test/java/org/eclipse/che/multiuser/api/permission/server/HttpPermissionCheckerImplTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server;
+
+import static java.util.Arrays.asList;
+import static org.mockito.ArgumentMatchers.anyObject;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.RETURNS_DEFAULTS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+import jakarta.ws.rs.core.UriBuilder;
+import org.eclipse.che.api.core.rest.HttpJsonRequest;
+import org.eclipse.che.api.core.rest.HttpJsonRequestFactory;
+import org.eclipse.che.api.core.rest.HttpJsonResponse;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.api.permission.shared.dto.PermissionsDto;
+import org.mockito.Mock;
+import org.mockito.stubbing.Answer;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link HttpPermissionCheckerImpl}.
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners(MockitoTestNGListener.class)
+public class HttpPermissionCheckerImplTest {
+ private static final String API_ENDPOINT = "http://localhost:8000/api";
+
+ @Mock private HttpJsonRequestFactory requestFactory;
+ @Mock private HttpJsonResponse response;
+ private HttpJsonRequest request;
+
+ private HttpPermissionCheckerImpl httpPermissionChecker;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ request =
+ mock(
+ HttpJsonRequest.class,
+ (Answer)
+ invocation -> {
+ if (invocation.getMethod().getReturnType().isInstance(invocation.getMock())) {
+ return invocation.getMock();
+ }
+ return RETURNS_DEFAULTS.answer(invocation);
+ });
+ when(request.request()).thenReturn(response);
+ when(requestFactory.fromUrl(anyString())).thenReturn(request);
+
+ httpPermissionChecker = new HttpPermissionCheckerImpl(API_ENDPOINT, requestFactory);
+ }
+
+ @Test
+ public void shouldCheckPermissionsByHttpRequestToPermissionsService() throws Exception {
+ when(response.asDto(anyObject()))
+ .thenReturn(
+ DtoFactory.newDto(PermissionsDto.class)
+ .withUserId("user123")
+ .withDomainId("domain123")
+ .withInstanceId("instance123")
+ .withActions(asList("read", "test")));
+
+ final boolean hasPermission =
+ httpPermissionChecker.hasPermission("user123", "domain123", "instance123", "test");
+
+ assertEquals(hasPermission, true);
+ verify(requestFactory)
+ .fromUrl(
+ eq(
+ UriBuilder.fromUri(API_ENDPOINT)
+ .path(PermissionsService.class)
+ .path(PermissionsService.class, "getCurrentUsersPermissions")
+ .queryParam("instance", "instance123")
+ .build("domain123")
+ .toString()));
+ verify(request).useGetMethod();
+ verify(request).request();
+ verifyNoMoreInteractions(request);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-authorization/src/test/resources/logback-test.xml b/multiuser/api/che-multiuser-api-authorization/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..d7908d57513
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-authorization/src/test/resources/logback-test.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+ %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n
+
+
+
+
+ target/log/api-authorization.log
+
+ %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n
+
+
+
+
+
+
+
+
+
diff --git a/multiuser/api/che-multiuser-api-organization-shared/pom.xml b/multiuser/api/che-multiuser-api-organization-shared/pom.xml
new file mode 100644
index 00000000000..73b1465ec3f
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/pom.xml
@@ -0,0 +1,51 @@
+
+
+
+ 4.0.0
+
+ che-multiuser-api
+ org.eclipse.che.multiuser
+ 7.103.0-SNAPSHOT
+
+ che-multiuser-api-organization-shared
+ jar
+ Che Multiuser :: Organization :: Shared
+
+
+ org.eclipse.che.core
+ che-core-api-core
+
+
+ org.eclipse.che.core
+ che-core-api-dto
+
+
+ org.eclipse.che.core
+ che-core-api-model
+
+
+ org.eclipse.che.core
+ che-core-api-user-shared
+
+
+ org.eclipse.che.core
+ che-core-commons-annotations
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-resource-shared
+
+
+
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/Constants.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/Constants.java
new file mode 100644
index 00000000000..d66ac852061
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/Constants.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared;
+
+/**
+ * Constants for Organization API
+ *
+ * @author Sergii Leschenko
+ */
+public final class Constants {
+ public static final String LINK_REL_SELF = "self";
+ public static final String LINK_REL_SUBORGANIZATIONS = "organization.suborganizations";
+
+ private Constants() {}
+}
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/MemberAddedEventDto.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/MemberAddedEventDto.java
new file mode 100644
index 00000000000..0c1e8b8b9f6
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/MemberAddedEventDto.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared.dto;
+
+import org.eclipse.che.api.core.notification.EventOrigin;
+import org.eclipse.che.api.user.shared.dto.UserDto;
+import org.eclipse.che.dto.shared.DTO;
+import org.eclipse.che.multiuser.organization.shared.event.EventType;
+
+/**
+ * DTO for member added event.
+ *
+ * @author Anton Korneta
+ */
+@DTO
+@EventOrigin("organization")
+public interface MemberAddedEventDto extends OrganizationEventDto {
+
+ @Override
+ MemberAddedEventDto withOrganization(OrganizationDto organization);
+
+ @Override
+ MemberAddedEventDto withType(EventType eventType);
+
+ UserDto getMember();
+
+ void setMember(UserDto member);
+
+ MemberAddedEventDto withMember(UserDto member);
+
+ /** Returns name of user who initiated member invitation */
+ String getInitiator();
+
+ void setInitiator(String initiator);
+
+ MemberAddedEventDto withInitiator(String initiator);
+}
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/MemberRemovedEventDto.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/MemberRemovedEventDto.java
new file mode 100644
index 00000000000..293b15dd8ba
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/MemberRemovedEventDto.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared.dto;
+
+import org.eclipse.che.api.core.notification.EventOrigin;
+import org.eclipse.che.api.user.shared.dto.UserDto;
+import org.eclipse.che.dto.shared.DTO;
+import org.eclipse.che.multiuser.organization.shared.event.EventType;
+
+/**
+ * DTO for organization member removed event.
+ *
+ * @author Anton Korneta
+ */
+@DTO
+@EventOrigin("organization")
+public interface MemberRemovedEventDto extends OrganizationEventDto {
+
+ @Override
+ MemberRemovedEventDto withOrganization(OrganizationDto organization);
+
+ @Override
+ MemberRemovedEventDto withType(EventType eventType);
+
+ UserDto getMember();
+
+ void setMember(UserDto member);
+
+ MemberRemovedEventDto withMember(UserDto member);
+
+ /** Returns name of user who initiated member removal */
+ String getInitiator();
+
+ void setInitiator(String initiator);
+
+ MemberRemovedEventDto withInitiator(String initiator);
+}
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationDistributedResourcesDto.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationDistributedResourcesDto.java
new file mode 100644
index 00000000000..09358a88f87
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationDistributedResourcesDto.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared.dto;
+
+import java.util.List;
+import org.eclipse.che.dto.shared.DTO;
+import org.eclipse.che.multiuser.organization.shared.model.OrganizationDistributedResources;
+import org.eclipse.che.multiuser.resource.shared.dto.ResourceDto;
+
+/** @author Sergii Leschenko */
+@DTO
+public interface OrganizationDistributedResourcesDto extends OrganizationDistributedResources {
+ @Override
+ String getOrganizationId();
+
+ void setOrganizationId(String organizationId);
+
+ OrganizationDistributedResourcesDto withOrganizationId(String organizationId);
+
+ @Override
+ List getResourcesCap();
+
+ void setResourcesCap(List resourcesCap);
+
+ OrganizationDistributedResourcesDto withResourcesCap(List resourcesCap);
+}
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationDto.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationDto.java
new file mode 100644
index 00000000000..78ff2e32544
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationDto.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared.dto;
+
+import java.util.List;
+import org.eclipse.che.api.core.rest.shared.dto.Hyperlinks;
+import org.eclipse.che.api.core.rest.shared.dto.Link;
+import org.eclipse.che.dto.shared.DTO;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+
+/** @author Sergii Leschenko */
+@DTO
+public interface OrganizationDto extends Organization, Hyperlinks {
+ @Override
+ String getId();
+
+ void setId(String id);
+
+ OrganizationDto withId(String id);
+
+ @Override
+ String getName();
+
+ void setName(String name);
+
+ OrganizationDto withName(String name);
+
+ @Override
+ String getQualifiedName();
+
+ void setQualifiedName(String qualifiedName);
+
+ OrganizationDto withQualifiedName(String qualifiedName);
+
+ @Override
+ String getParent();
+
+ void setParent(String parent);
+
+ OrganizationDto withParent(String parent);
+
+ @Override
+ OrganizationDto withLinks(List links);
+}
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationEventDto.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationEventDto.java
new file mode 100644
index 00000000000..deea6789ee7
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationEventDto.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared.dto;
+
+import org.eclipse.che.api.core.notification.EventOrigin;
+import org.eclipse.che.dto.shared.DTO;
+import org.eclipse.che.multiuser.organization.shared.event.EventType;
+import org.eclipse.che.multiuser.organization.shared.event.OrganizationEvent;
+
+/**
+ * DTO for {@link OrganizationEvent}.
+ *
+ * @author Anton Korneta
+ */
+@DTO
+@EventOrigin("organization")
+public interface OrganizationEventDto extends OrganizationEvent {
+
+ @Override
+ OrganizationDto getOrganization();
+
+ void setOrganization(OrganizationDto organization);
+
+ OrganizationEventDto withOrganization(OrganizationDto organization);
+
+ void setType(EventType eventType);
+
+ OrganizationEventDto withType(EventType eventType);
+}
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationRemovedEventDto.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationRemovedEventDto.java
new file mode 100644
index 00000000000..5e996a6bf06
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationRemovedEventDto.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared.dto;
+
+import java.util.List;
+import org.eclipse.che.api.core.notification.EventOrigin;
+import org.eclipse.che.dto.shared.DTO;
+import org.eclipse.che.multiuser.organization.shared.event.EventType;
+
+/**
+ * DTO for organization removed event.
+ *
+ * @author Anton Korneta
+ */
+@DTO
+@EventOrigin("organization")
+public interface OrganizationRemovedEventDto extends OrganizationEventDto {
+
+ @Override
+ OrganizationRemovedEventDto withOrganization(OrganizationDto organization);
+
+ @Override
+ OrganizationRemovedEventDto withType(EventType eventType);
+
+ /** Returns name of user who initiated organization removal */
+ String getInitiator();
+
+ void setInitiator(String initiator);
+
+ OrganizationRemovedEventDto withInitiator(String initiator);
+
+ List getMembers();
+
+ void setMembers(List members);
+
+ OrganizationRemovedEventDto withMembers(List members);
+}
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationRenamedEventDto.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationRenamedEventDto.java
new file mode 100644
index 00000000000..bb9b27faf44
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/dto/OrganizationRenamedEventDto.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared.dto;
+
+import org.eclipse.che.api.core.notification.EventOrigin;
+import org.eclipse.che.dto.shared.DTO;
+import org.eclipse.che.multiuser.organization.shared.event.EventType;
+
+/**
+ * DTO for organization renamed event.
+ *
+ * @author Anton Korneta
+ */
+@DTO
+@EventOrigin("organization")
+public interface OrganizationRenamedEventDto extends OrganizationEventDto {
+
+ @Override
+ OrganizationRenamedEventDto withOrganization(OrganizationDto organization);
+
+ @Override
+ OrganizationRenamedEventDto withType(EventType eventType);
+
+ /** Returns organization name before renaming */
+ String getOldName();
+
+ void setOldName(String oldName);
+
+ OrganizationRenamedEventDto withOldName(String oldName);
+
+ /** Returns organization name after renaming */
+ String getNewName();
+
+ void setNewName(String newName);
+
+ OrganizationRenamedEventDto withNewName(String newName);
+
+ /** Returns name of user who initiated organization rename */
+ String getInitiator();
+
+ void setInitiator(String initiator);
+
+ OrganizationRenamedEventDto withInitiator(String initiator);
+}
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/event/EventType.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/event/EventType.java
new file mode 100644
index 00000000000..ce734a2e4e7
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/event/EventType.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared.event;
+
+/**
+ * Defines organizations event types.
+ *
+ * @author Anton Korneta
+ */
+public enum EventType {
+
+ /** Published when organization name changed. */
+ ORGANIZATION_RENAMED,
+
+ /** Published when organization removed. */
+ ORGANIZATION_REMOVED,
+
+ /** Published when new member added to organization. */
+ MEMBER_ADDED,
+
+ /** Published when member removed from organization. */
+ MEMBER_REMOVED
+}
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/event/MemberEvent.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/event/MemberEvent.java
new file mode 100644
index 00000000000..27c36840ba0
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/event/MemberEvent.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared.event;
+
+import org.eclipse.che.api.core.model.user.User;
+
+/**
+ * Defines organization member event.
+ *
+ * @author Anton Korneta
+ */
+public interface MemberEvent extends OrganizationEvent {
+
+ /** Returns the member associated with this event. */
+ User getMember();
+}
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/event/OrganizationEvent.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/event/OrganizationEvent.java
new file mode 100644
index 00000000000..1956c705ffc
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/event/OrganizationEvent.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared.event;
+
+import org.eclipse.che.commons.annotation.Nullable;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+
+/**
+ * The base interface for organization event.
+ *
+ * @author Anton Korneta
+ */
+public interface OrganizationEvent {
+
+ /** Returns organization related to this event. */
+ Organization getOrganization();
+
+ /** Returns type of this event. */
+ EventType getType();
+
+ /** Returns name of user who acted with organization or null if user is undefined. */
+ @Nullable
+ String getInitiator();
+}
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/model/Member.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/model/Member.java
new file mode 100644
index 00000000000..b4f0c66b592
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/model/Member.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared.model;
+
+import java.util.List;
+
+/**
+ * Describes relations of user and organization
+ *
+ * @author gazarenkov
+ * @author Sergii Leschenko
+ */
+public interface Member {
+ /** Returns id of user */
+ String getUserId();
+
+ /** Returns id of organization */
+ String getOrganizationId();
+
+ /** Returns list of actions that user can perform in organization */
+ List getActions();
+}
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/model/Organization.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/model/Organization.java
new file mode 100644
index 00000000000..60946cf274b
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/model/Organization.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared.model;
+
+import org.eclipse.che.commons.annotation.Nullable;
+
+/**
+ * Describes group of users that can use common resources
+ *
+ * @author gazarenkov
+ * @author Sergii Leschenko
+ */
+public interface Organization {
+
+ /**
+ * Returns the identifier of the organization (e.g. "organization0x1234567890"). The identifier
+ * value is unique and mandatory.
+ */
+ String getId();
+
+ /**
+ * Returns name of organization. The name is mandatory and updatable. The name is unique per
+ * parent organization.
+ */
+ String getName();
+
+ /**
+ * Returns the qualified name that includes all parent's names and the name of current
+ * organization separated by '/' symbol e.g. "parentOrgName/subOrgName/subSubOrgName". The
+ * qualified name is unique.
+ */
+ String getQualifiedName();
+
+ /**
+ * Returns id of parent organization. The returned value can be nullable in case when organization
+ * is root
+ */
+ @Nullable
+ String getParent();
+}
diff --git a/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/model/OrganizationDistributedResources.java b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/model/OrganizationDistributedResources.java
new file mode 100644
index 00000000000..4605d94f868
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization-shared/src/main/java/org/eclipse/che/multiuser/organization/shared/model/OrganizationDistributedResources.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.shared.model;
+
+import java.util.List;
+import org.eclipse.che.multiuser.resource.model.Resource;
+
+/**
+ * Defines resources which are distributed for suborganization by parent organization
+ *
+ * @author Sergii Leschenko
+ */
+public interface OrganizationDistributedResources {
+ /** Id of organization that owns these distributed resources */
+ String getOrganizationId();
+
+ /**
+ * Returns resources cap that limit usage of parent organization's resources.
+ *
+ *
Note that suborganization is not limited to use parent organization's resources if resource
+ * is not capped.
+ */
+ List extends Resource> getResourcesCap();
+}
diff --git a/multiuser/api/che-multiuser-api-organization/pom.xml b/multiuser/api/che-multiuser-api-organization/pom.xml
new file mode 100644
index 00000000000..8b6dde36afd
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/pom.xml
@@ -0,0 +1,313 @@
+
+
+
+ 4.0.0
+
+ che-multiuser-api
+ org.eclipse.che.multiuser
+ 7.103.0-SNAPSHOT
+
+ che-multiuser-api-organization
+ jar
+ Che Multiuser :: Organization
+
+ ${project.build.directory}/generated-sources/dto/
+
+
+
+ com.google.code.gson
+ gson
+
+
+ com.google.guava
+ guava
+
+
+ com.google.inject
+ guice
+
+
+ com.google.inject.extensions
+ guice-persist
+
+
+ io.swagger.core.v3
+ swagger-annotations-jakarta
+
+
+ jakarta.annotation
+ jakarta.annotation-api
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+
+
+ org.eclipse.che.core
+ che-core-api-account
+
+
+ org.eclipse.che.core
+ che-core-api-core
+
+
+ org.eclipse.che.core
+ che-core-api-dto
+
+
+ org.eclipse.che.core
+ che-core-api-model
+
+
+ org.eclipse.che.core
+ che-core-api-user
+
+
+ org.eclipse.che.core
+ che-core-api-user-shared
+
+
+ org.eclipse.che.core
+ che-core-commons-annotations
+
+
+ org.eclipse.che.core
+ che-core-commons-lang
+
+
+ org.eclipse.che.core
+ che-core-commons-test
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-organization-shared
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-permission
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-permission-shared
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-resource
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-resource-shared
+
+
+ org.eclipse.persistence
+ jakarta.persistence
+
+
+ org.everrest
+ everrest-core
+
+
+ org.slf4j
+ slf4j-api
+
+
+ jakarta.websocket
+ jakarta.websocket-api
+ provided
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+ com.h2database
+ h2
+ test
+
+
+ commons-fileupload
+ commons-fileupload
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ org.eclipse.che.core
+ che-core-sql-schema
+ test
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-sql-schema
+ test
+
+
+ org.eclipse.persistence
+ org.eclipse.persistence.core
+ test
+
+
+ org.eclipse.persistence
+ org.eclipse.persistence.jpa
+ test
+
+
+ org.everrest
+ everrest-assured
+ test
+
+
+ org.hamcrest
+ hamcrest
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-testng
+ test
+
+
+ org.testng
+ testng
+ test
+
+
+
+
+
+ org.eclipse.che.core
+ che-core-api-dto-maven-plugin
+ ${project.version}
+
+
+ process-sources
+
+ generate
+
+
+
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-organization-shared
+ ${project.version}
+
+
+
+
+ org.eclipse.che.multiuser.organization.shared.dto
+
+ ${dto-generator-out-directory}
+ org.eclipse.che.multiuser.organization.api.dto.DtoServerImpls
+ server
+
+
+
+ maven-compiler-plugin
+
+
+ pre-compile
+ generate-sources
+
+ compile
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ add-domain
+ process-sources
+
+ add-resource
+
+
+
+
+ ${dto-generator-out-directory}/META-INF
+ META-INF
+
+
+
+
+
+ add-source
+ process-sources
+
+ add-source
+
+
+
+ ${dto-generator-out-directory}
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+ **/spi/tck/*.*
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ resource-dependencies
+ process-test-resources
+
+ unpack-dependencies
+
+
+ che-core-sql-schema,
+ che-multiuser-sql-schema
+ che-schema/
+ ${project.build.directory}
+
+
+
+
+
+
+
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/DtoConverter.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/DtoConverter.java
new file mode 100644
index 00000000000..0bb08f445b4
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/DtoConverter.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api;
+
+import static org.eclipse.che.dto.server.DtoFactory.newDto;
+
+import java.util.stream.Collectors;
+import org.eclipse.che.multiuser.organization.api.event.MemberAddedEvent;
+import org.eclipse.che.multiuser.organization.api.event.MemberRemovedEvent;
+import org.eclipse.che.multiuser.organization.api.event.OrganizationRemovedEvent;
+import org.eclipse.che.multiuser.organization.api.event.OrganizationRenamedEvent;
+import org.eclipse.che.multiuser.organization.shared.dto.MemberAddedEventDto;
+import org.eclipse.che.multiuser.organization.shared.dto.MemberRemovedEventDto;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationDistributedResourcesDto;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationDto;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationEventDto;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationRemovedEventDto;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationRenamedEventDto;
+import org.eclipse.che.multiuser.organization.shared.event.OrganizationEvent;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+import org.eclipse.che.multiuser.organization.shared.model.OrganizationDistributedResources;
+
+/**
+ * Helps to convert objects related to organization to DTOs.
+ *
+ * @author Sergii Leschenko
+ */
+public final class DtoConverter {
+ private DtoConverter() {}
+
+ public static OrganizationDto asDto(Organization organization) {
+ return newDto(OrganizationDto.class)
+ .withId(organization.getId())
+ .withName(organization.getName())
+ .withQualifiedName(organization.getQualifiedName())
+ .withParent(organization.getParent());
+ }
+
+ public static OrganizationDistributedResourcesDto asDto(
+ OrganizationDistributedResources distributedResources) {
+ return newDto(OrganizationDistributedResourcesDto.class)
+ .withOrganizationId(distributedResources.getOrganizationId())
+ .withResourcesCap(
+ distributedResources.getResourcesCap().stream()
+ .map(org.eclipse.che.multiuser.resource.api.DtoConverter::asDto)
+ .collect(Collectors.toList()));
+ }
+
+ public static OrganizationRemovedEventDto asDto(OrganizationRemovedEvent event) {
+ return newDto(OrganizationRemovedEventDto.class)
+ .withType(event.getType())
+ .withOrganization(asDto(event.getOrganization()))
+ .withMembers(event.getMembers())
+ .withInitiator(event.getInitiator());
+ }
+
+ public static OrganizationRenamedEventDto asDto(OrganizationRenamedEvent event) {
+ return newDto(OrganizationRenamedEventDto.class)
+ .withType(event.getType())
+ .withOrganization(asDto(event.getOrganization()))
+ .withOldName(event.getOldName())
+ .withNewName(event.getNewName())
+ .withInitiator(event.getInitiator());
+ }
+
+ public static MemberAddedEventDto asDto(MemberAddedEvent event) {
+ return newDto(MemberAddedEventDto.class)
+ .withType(event.getType())
+ .withOrganization(asDto(event.getOrganization()))
+ .withInitiator(event.getInitiator())
+ .withMember(org.eclipse.che.api.user.server.DtoConverter.asDto(event.getMember()));
+ }
+
+ public static MemberRemovedEventDto asDto(MemberRemovedEvent event) {
+ return newDto(MemberRemovedEventDto.class)
+ .withType(event.getType())
+ .withOrganization(asDto(event.getOrganization()))
+ .withInitiator((event.getInitiator()))
+ .withMember(org.eclipse.che.api.user.server.DtoConverter.asDto(event.getMember()));
+ }
+
+ public static OrganizationEventDto asDto(OrganizationEvent event) {
+ switch (event.getType()) {
+ case ORGANIZATION_RENAMED:
+ return asDto((OrganizationRenamedEvent) event);
+ case ORGANIZATION_REMOVED:
+ return asDto((OrganizationRemovedEvent) event);
+ case MEMBER_ADDED:
+ return asDto((MemberAddedEvent) event);
+ case MEMBER_REMOVED:
+ return asDto((MemberRemovedEvent) event);
+ default:
+ throw new IllegalArgumentException(
+ "Can't convert event to dto, event type '" + event.getType() + "' is unknown");
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationApiModule.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationApiModule.java
new file mode 100644
index 00000000000..7471b576b53
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationApiModule.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.MapBinder;
+import com.google.inject.multibindings.Multibinder;
+import com.google.inject.name.Names;
+import org.eclipse.che.multiuser.api.permission.server.SuperPrivilegesChecker;
+import org.eclipse.che.multiuser.api.permission.server.account.AccountPermissionsChecker;
+import org.eclipse.che.multiuser.api.permission.shared.model.PermissionsDomain;
+import org.eclipse.che.multiuser.organization.api.listener.MemberEventsPublisher;
+import org.eclipse.che.multiuser.organization.api.listener.OrganizationEventsWebsocketBroadcaster;
+import org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain;
+import org.eclipse.che.multiuser.organization.api.permissions.OrganizationPermissionsFilter;
+import org.eclipse.che.multiuser.organization.api.permissions.OrganizationRemoteSubscriptionPermissionsChecks;
+import org.eclipse.che.multiuser.organization.api.permissions.OrganizationResourceDistributionServicePermissionsFilter;
+import org.eclipse.che.multiuser.organization.api.permissions.OrganizationalAccountPermissionsChecker;
+import org.eclipse.che.multiuser.organization.api.resource.DefaultOrganizationResourcesProvider;
+import org.eclipse.che.multiuser.organization.api.resource.OrganizationResourceLockKeyProvider;
+import org.eclipse.che.multiuser.organization.api.resource.OrganizationalAccountAvailableResourcesProvider;
+import org.eclipse.che.multiuser.organization.api.resource.SuborganizationResourcesProvider;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+import org.eclipse.che.multiuser.resource.api.AvailableResourcesProvider;
+import org.eclipse.che.multiuser.resource.api.ResourceLockKeyProvider;
+import org.eclipse.che.multiuser.resource.api.ResourcesProvider;
+import org.eclipse.che.multiuser.resource.api.free.DefaultResourcesProvider;
+
+/** @author Sergii Leschenko */
+public class OrganizationApiModule extends AbstractModule {
+ @Override
+ protected void configure() {
+
+ bind(OrganizationPermissionsFilter.class);
+ bind(OrganizationRemoteSubscriptionPermissionsChecks.class);
+
+ Multibinder.newSetBinder(binder(), DefaultResourcesProvider.class)
+ .addBinding()
+ .to(DefaultOrganizationResourcesProvider.class);
+
+ Multibinder.newSetBinder(binder(), ResourcesProvider.class)
+ .addBinding()
+ .to(SuborganizationResourcesProvider.class);
+
+ MapBinder.newMapBinder(binder(), String.class, AvailableResourcesProvider.class)
+ .addBinding(OrganizationImpl.ORGANIZATIONAL_ACCOUNT)
+ .to(OrganizationalAccountAvailableResourcesProvider.class);
+
+ Multibinder.newSetBinder(binder(), ResourceLockKeyProvider.class)
+ .addBinding()
+ .to(OrganizationResourceLockKeyProvider.class);
+
+ Multibinder.newSetBinder(binder(), AccountPermissionsChecker.class)
+ .addBinding()
+ .to(OrganizationalAccountPermissionsChecker.class);
+
+ bind(OrganizationResourceDistributionServicePermissionsFilter.class);
+
+ bind(OrganizationEventsWebsocketBroadcaster.class).asEagerSingleton();
+ bind(MemberEventsPublisher.class).asEagerSingleton();
+
+ Multibinder.newSetBinder(
+ binder(),
+ PermissionsDomain.class,
+ Names.named(SuperPrivilegesChecker.SUPER_PRIVILEGED_DOMAINS))
+ .addBinding()
+ .to(OrganizationDomain.class);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationJpaModule.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationJpaModule.java
new file mode 100644
index 00000000000..349d7c7b001
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationJpaModule.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.TypeLiteral;
+import org.eclipse.che.multiuser.api.permission.server.AbstractPermissionsDomain;
+import org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain;
+import org.eclipse.che.multiuser.organization.spi.impl.MemberImpl;
+
+/** @author Sergii Leschenko */
+public class OrganizationJpaModule extends AbstractModule {
+ @Override
+ protected void configure() {
+
+ bind(new TypeLiteral>() {}).to(OrganizationDomain.class);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationLinksInjector.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationLinksInjector.java
new file mode 100644
index 00000000000..aba39ac11ab
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationLinksInjector.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api;
+
+import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
+
+import jakarta.ws.rs.HttpMethod;
+import jakarta.ws.rs.core.UriBuilder;
+import java.util.ArrayList;
+import java.util.List;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.rest.ServiceContext;
+import org.eclipse.che.api.core.rest.shared.dto.Link;
+import org.eclipse.che.api.core.util.LinksHelper;
+import org.eclipse.che.multiuser.organization.shared.Constants;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationDto;
+
+/**
+ * Helps to inject {@link OrganizationService} related links.
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class OrganizationLinksInjector {
+ public OrganizationDto injectLinks(
+ OrganizationDto organizationDto, ServiceContext serviceContext) {
+ final UriBuilder uriBuilder = serviceContext.getBaseUriBuilder();
+ final List links = new ArrayList<>(2);
+ links.add(
+ LinksHelper.createLink(
+ HttpMethod.GET,
+ uriBuilder
+ .clone()
+ .path(OrganizationService.class)
+ .path(OrganizationService.class, "getById")
+ .build(organizationDto.getId())
+ .toString(),
+ null,
+ APPLICATION_JSON,
+ Constants.LINK_REL_SELF));
+ links.add(
+ LinksHelper.createLink(
+ HttpMethod.GET,
+ uriBuilder
+ .clone()
+ .path(OrganizationService.class)
+ .path(OrganizationService.class, "getByParent")
+ .build(organizationDto.getId())
+ .toString(),
+ null,
+ APPLICATION_JSON,
+ Constants.LINK_REL_SUBORGANIZATIONS));
+ return organizationDto.withLinks(links);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationManager.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationManager.java
new file mode 100644
index 00000000000..2bebedbf1fa
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationManager.java
@@ -0,0 +1,336 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api;
+
+import static java.util.Objects.requireNonNull;
+import static org.eclipse.che.multiuser.organization.api.DtoConverter.asDto;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.collect.Sets;
+import com.google.inject.persist.Transactional;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.ApiException;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.Pages;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.notification.EventService;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.lang.NameGenerator;
+import org.eclipse.che.multiuser.organization.api.event.OrganizationRemovedEvent;
+import org.eclipse.che.multiuser.organization.api.event.OrganizationRenamedEvent;
+import org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain;
+import org.eclipse.che.multiuser.organization.shared.model.Member;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+import org.eclipse.che.multiuser.organization.spi.MemberDao;
+import org.eclipse.che.multiuser.organization.spi.OrganizationDao;
+import org.eclipse.che.multiuser.organization.spi.impl.MemberImpl;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+
+/**
+ * Facade for Organization related operations.
+ *
+ * @author gazarenkov
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class OrganizationManager {
+
+ private final EventService eventService;
+ private final OrganizationDao organizationDao;
+ private final MemberDao memberDao;
+ private final Set reservedNames;
+
+ @Inject
+ public OrganizationManager(
+ EventService eventService,
+ OrganizationDao organizationDao,
+ MemberDao memberDao,
+ @Named("che.auth.reserved_user_names") String[] reservedNames) {
+ this.eventService = eventService;
+ this.organizationDao = organizationDao;
+ this.memberDao = memberDao;
+ this.reservedNames = Sets.newHashSet(reservedNames);
+ }
+
+ /**
+ * Creates new organization.
+ *
+ * @param newOrganization organization to create
+ * @return created organization
+ * @throws NullPointerException when {@code organization} is null
+ * @throws NotFoundException when parent organization was not found
+ * @throws ConflictException when organization with such id/name already exists
+ * @throws ConflictException when specified organization name is reserved
+ * @throws ServerException when any other error occurs during organization creation
+ */
+ @Transactional(rollbackOn = {RuntimeException.class, ApiException.class})
+ public Organization create(Organization newOrganization)
+ throws NotFoundException, ConflictException, ServerException {
+ requireNonNull(newOrganization, "Required non-null organization");
+ requireNonNull(newOrganization.getName(), "Required non-null organization name");
+
+ String qualifiedName;
+ if (newOrganization.getParent() != null) {
+ final Organization parent = getById(newOrganization.getParent());
+ qualifiedName = parent.getQualifiedName() + "/" + newOrganization.getName();
+ } else {
+ qualifiedName = newOrganization.getName();
+ }
+ checkNameReservation(qualifiedName);
+
+ final OrganizationImpl organization =
+ new OrganizationImpl(
+ NameGenerator.generate("organization", 16), qualifiedName, newOrganization.getParent());
+ organizationDao.create(organization);
+ addFirstMember(organization);
+ return organization;
+ }
+
+ /**
+ * Updates organization with new entity.
+ *
+ * @param organizationId id of organization to update
+ * @param update organization update
+ * @throws NullPointerException when {@code organizationId} or {@code update} is null
+ * @throws NotFoundException when organization with given id doesn't exist
+ * @throws ConflictException when name updated with a value which is reserved or is not unique
+ * @throws ServerException when any other error occurs organization updating
+ */
+ @Transactional(rollbackOn = {RuntimeException.class, ApiException.class})
+ public Organization update(String organizationId, Organization update)
+ throws NotFoundException, ConflictException, ServerException {
+ requireNonNull(organizationId, "Required non-null organization id");
+ requireNonNull(update, "Required non-null organization");
+ requireNonNull(update.getName(), "Required non-null organization name");
+
+ final OrganizationImpl organization = organizationDao.getById(organizationId);
+ final String oldQualifiedName = organization.getQualifiedName();
+ final String oldName = organization.getName();
+
+ final String newName = update.getName();
+ final String newQualifiedName = buildQualifiedName(oldQualifiedName, update.getName());
+
+ checkNameReservation(newQualifiedName);
+ organization.setQualifiedName(newQualifiedName);
+
+ organizationDao.update(organization);
+ if (!newName.equals(oldName)) {
+ updateSuborganizationsQualifiedNames(oldQualifiedName, organization.getQualifiedName());
+
+ final String performerName = EnvironmentContext.getCurrent().getSubject().getUserName();
+ // should be DTO as it sent via json rpc
+ eventService.publish(
+ asDto(new OrganizationRenamedEvent(performerName, oldName, newName, organization)));
+ }
+ return organization;
+ }
+
+ /**
+ * Removes organization with given id
+ *
+ * @param organizationId organization id
+ * @throws NullPointerException when {@code organizationId} is null
+ * @throws ServerException when any other error occurs during organization removing
+ */
+ @Transactional(rollbackOn = {RuntimeException.class, ApiException.class})
+ public void remove(String organizationId) throws ServerException {
+ requireNonNull(organizationId, "Required non-null organization id");
+ try {
+ OrganizationImpl organization = organizationDao.getById(organizationId);
+ removeSuborganizations(organizationId);
+ final List members = removeMembers(organizationId);
+ organizationDao.remove(organizationId);
+ final String initiator = EnvironmentContext.getCurrent().getSubject().getUserName();
+ eventService.publish(asDto(new OrganizationRemovedEvent(initiator, organization, members)));
+ } catch (NotFoundException e) {
+ // organization is already removed
+ }
+ }
+
+ /**
+ * Gets organization by identifier.
+ *
+ * @param organizationId organization id
+ * @return organization instance
+ * @throws NullPointerException when {@code organizationId} is null
+ * @throws NotFoundException when organization with given id was not found
+ * @throws ServerException when any other error occurs during organization fetching
+ */
+ public Organization getById(String organizationId) throws NotFoundException, ServerException {
+ requireNonNull(organizationId, "Required non-null organization id");
+ return organizationDao.getById(organizationId);
+ }
+
+ /**
+ * Gets organization by name.
+ *
+ * @param organizationName organization name
+ * @return organization instance
+ * @throws NullPointerException when {@code organizationName} is null
+ * @throws NotFoundException when organization with given name was not found
+ * @throws ServerException when any other error occurs during organization fetching
+ */
+ public Organization getByName(String organizationName) throws NotFoundException, ServerException {
+ requireNonNull(organizationName, "Required non-null organization name");
+ return organizationDao.getByName(organizationName);
+ }
+
+ /**
+ * Gets child organizations by given parent.
+ *
+ * @param parent id of parent organizations
+ * @param maxItems the maximum number of organizations to return
+ * @param skipCount the number of organizations to skip
+ * @return list of children organizations
+ * @throws NullPointerException when {@code parent} is null
+ * @throws ServerException when any other error occurs during organizations fetching
+ */
+ public Page extends Organization> getByParent(String parent, int maxItems, long skipCount)
+ throws ServerException {
+ requireNonNull(parent, "Required non-null parent");
+ return organizationDao.getByParent(parent, maxItems, skipCount);
+ }
+
+ /**
+ * Gets all child organizations by specified parent qualified name.
+ *
+ *
Note that the result will includes all direct and nested suborganizations.
+ *
+ * @param parentQualifiedName qualified name of parent organization
+ * @param maxItems the maximum number of organizations to return
+ * @param skipCount the number of organizations to skip
+ * @return list of children organizations
+ * @throws NullPointerException when {@code parentQualifiedName} is null
+ * @throws ServerException when any other error occurs during organizations fetching
+ */
+ public Page getSuborganizations(
+ String parentQualifiedName, int maxItems, long skipCount) throws ServerException {
+ requireNonNull(parentQualifiedName, "Required non-null parent qualified name");
+ return organizationDao.getSuborganizations(parentQualifiedName, maxItems, skipCount);
+ }
+
+ /**
+ * Gets list organizations where user is member.
+ *
+ * @param userId user id
+ * @param maxItems the maximum number of organizations to return
+ * @param skipCount the number of organizations to skip
+ * @return list of organizations where user is member
+ * @throws NullPointerException when {@code userId} is null
+ * @throws ServerException when any other error occurs during organizations fetching
+ */
+ public Page extends Organization> getByMember(String userId, int maxItems, int skipCount)
+ throws ServerException {
+ requireNonNull(userId, "Required non-null user id");
+ return memberDao.getOrganizations(userId, maxItems, skipCount);
+ }
+
+ private String buildQualifiedName(String oldQualifiedName, String newName) {
+ int lastSlashIndex = oldQualifiedName.lastIndexOf("/");
+ if (lastSlashIndex != -1) { // check that it is not root organization
+ return oldQualifiedName.substring(0, lastSlashIndex + 1) + newName;
+ } else {
+ return newName;
+ }
+ }
+
+ private void updateSuborganizationsQualifiedNames(
+ String oldQualifiedName, String newQualifiedName)
+ throws NotFoundException, ConflictException, ServerException {
+ for (OrganizationImpl suborganization :
+ Pages.iterate(
+ (maxItems, skipCount) ->
+ organizationDao.getSuborganizations(oldQualifiedName, maxItems, skipCount))) {
+ suborganization.setQualifiedName(
+ suborganization.getQualifiedName().replaceFirst(oldQualifiedName, newQualifiedName));
+ organizationDao.update(suborganization);
+ }
+ }
+
+ /**
+ * Gets list of members by specified organization id.
+ *
+ * @param organizationId organization identifier
+ * @param maxItems the maximum number of members to return
+ * @param skipCount the number of members to skip
+ * @return list of members
+ * @throws NullPointerException when {@code organizationId} is null
+ * @throws ServerException when any other error occurs during organizations fetching
+ */
+ public Page extends Member> getMembers(String organizationId, int maxItems, long skipCount)
+ throws ServerException {
+ requireNonNull(organizationId, "Required non-null organization id");
+ return memberDao.getMembers(organizationId, maxItems, skipCount);
+ }
+
+ protected void addFirstMember(Organization organization) throws ServerException {
+ memberDao.store(
+ new MemberImpl(
+ EnvironmentContext.getCurrent().getSubject().getUserId(),
+ organization.getId(),
+ OrganizationDomain.getActions()));
+ }
+
+ /**
+ * Removes suborganizations of given parent organization page by page
+ *
+ * @param organizationId parent organization id
+ */
+ @VisibleForTesting
+ void removeSuborganizations(String organizationId) throws ServerException {
+ Page extends Organization> suborganizationsPage;
+ do {
+ // skip count always equals to 0 because elements will be shifted after removing previous
+ // items
+ suborganizationsPage = organizationDao.getByParent(organizationId, 100, 0);
+ for (Organization suborganization : suborganizationsPage.getItems()) {
+ remove(suborganization.getId());
+ }
+ } while (suborganizationsPage.hasNextPage());
+ }
+
+ @VisibleForTesting
+ List removeMembers(String organizationId) throws ServerException {
+ List removed = new ArrayList<>();
+ Page membersPage;
+ do {
+ // skip count always equals to 0 because elements will be shifted after removing previous
+ // items
+ membersPage = memberDao.getMembers(organizationId, 100, 0);
+ for (MemberImpl member : membersPage.getItems()) {
+ removed.add(member.getUserId());
+ memberDao.remove(member.getUserId(), member.getOrganizationId());
+ }
+ } while (membersPage.hasNextPage());
+ return removed;
+ }
+
+ /**
+ * Checks reservation of organization name
+ *
+ * @param organizationName organization name to check
+ * @throws ConflictException when organization name is reserved and can be used by user
+ */
+ private void checkNameReservation(String organizationName) throws ConflictException {
+ if (reservedNames.contains(organizationName.toLowerCase())) {
+ throw new ConflictException(
+ String.format("Organization name '%s' is reserved", organizationName));
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationService.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationService.java
new file mode 100644
index 00000000000..904ed21bab9
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationService.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api;
+
+import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
+import static org.eclipse.che.multiuser.organization.api.DtoConverter.asDto;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.Response;
+import javax.inject.Inject;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.rest.Service;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationDto;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+
+/**
+ * Defines Organization REST API.
+ *
+ * @author Sergii Leschenko
+ */
+@Tag(name = "organization", description = "Organization REST API")
+@Path("/organization")
+public class OrganizationService extends Service {
+ private final OrganizationManager organizationManager;
+ private final OrganizationLinksInjector linksInjector;
+ private final OrganizationValidator organizationValidator;
+
+ @Inject
+ public OrganizationService(
+ OrganizationManager organizationManager,
+ OrganizationLinksInjector linksInjector,
+ OrganizationValidator organizationValidator) {
+ this.organizationManager = organizationManager;
+ this.linksInjector = linksInjector;
+ this.organizationValidator = organizationValidator;
+ }
+
+ @POST
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ @Operation(
+ summary = "Create new organization",
+ responses = {
+ @ApiResponse(
+ responseCode = "201",
+ description = "The organization successfully created",
+ content = @Content(schema = @Schema(implementation = OrganizationDto.class))),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Missed required parameters, parameters are not valid"),
+ @ApiResponse(
+ responseCode = "409",
+ description =
+ "Conflict error occurred during the organization creation"
+ + "(e.g. The organization with such name already exists)"),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public Response create(
+ @Parameter(description = "Organization to create", required = true)
+ OrganizationDto organization)
+ throws BadRequestException, NotFoundException, ConflictException, ServerException {
+ organizationValidator.checkOrganization(organization);
+ return Response.status(201)
+ .entity(
+ linksInjector.injectLinks(
+ asDto(organizationManager.create(organization)), getServiceContext()))
+ .build();
+ }
+
+ @POST
+ @Path("/{id}")
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ @Operation(
+ summary = "Update organization",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The organization successfully updated",
+ content = @Content(schema = @Schema(implementation = OrganizationDto.class))),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Missed required parameters, parameters are not valid"),
+ @ApiResponse(
+ responseCode = "404",
+ description = "The organization with given id was not found"),
+ @ApiResponse(
+ responseCode = "409",
+ description =
+ "Conflict error occurred during the organization creation"
+ + "(e.g. The organization with such name already exists)"),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public OrganizationDto update(
+ @Parameter(description = "Organization id") @PathParam("id") String organizationId,
+ @Parameter(description = "Organization to update", required = true)
+ OrganizationDto organization)
+ throws BadRequestException, ConflictException, NotFoundException, ServerException {
+ organizationValidator.checkOrganization(organization);
+ return linksInjector.injectLinks(
+ asDto(organizationManager.update(organizationId, organization)), getServiceContext());
+ }
+
+ @DELETE
+ @Path("/{id}")
+ @Operation(
+ summary = "Remove organization with given id",
+ responses = {
+ @ApiResponse(responseCode = "204", description = "The organization successfully removed"),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public void remove(
+ @Parameter(description = "Organization id") @PathParam("id") String organization)
+ throws ServerException {
+ organizationManager.remove(organization);
+ }
+
+ @GET
+ @Produces(APPLICATION_JSON)
+ @Path("/{organizationId}")
+ @Operation(
+ summary = "Get organization by id",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The organization successfully fetched",
+ content = @Content(schema = @Schema(implementation = OrganizationDto.class))),
+ @ApiResponse(
+ responseCode = "404",
+ description = "The organization with given id was not found"),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public OrganizationDto getById(
+ @Parameter(description = "Organization id") @PathParam("organizationId")
+ String organizationId)
+ throws NotFoundException, ServerException {
+ return linksInjector.injectLinks(
+ asDto(organizationManager.getById(organizationId)), getServiceContext());
+ }
+
+ @GET
+ @Produces(APPLICATION_JSON)
+ @Path("/find")
+ @Operation(
+ summary = "Find organization by name",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The organization successfully fetched",
+ content = @Content(schema = @Schema(implementation = OrganizationDto.class))),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Missed required parameters, parameters are not valid"),
+ @ApiResponse(
+ responseCode = "404",
+ description = "The organization with given name was not found"),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public OrganizationDto find(
+ @Parameter(description = "Organization name", required = true) @QueryParam("name")
+ String organizationName)
+ throws NotFoundException, ServerException, BadRequestException {
+ checkArgument(organizationName != null, "Missed organization's name");
+ return linksInjector.injectLinks(
+ asDto(organizationManager.getByName(organizationName)), getServiceContext());
+ }
+
+ @GET
+ @Produces(APPLICATION_JSON)
+ @Path("/{parent}/organizations")
+ @Operation(
+ summary = "Get child organizations",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The child organizations successfully fetched",
+ content =
+ @Content(
+ array =
+ @ArraySchema(schema = @Schema(implementation = OrganizationDto.class)))),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public Response getByParent(
+ @Parameter(description = "Parent organization id") @PathParam("parent") String parent,
+ @Parameter(description = "Max items") @QueryParam("maxItems") @DefaultValue("30")
+ int maxItems,
+ @Parameter(description = "Skip count") @QueryParam("skipCount") @DefaultValue("0")
+ int skipCount)
+ throws ServerException, BadRequestException {
+
+ checkArgument(maxItems >= 0, "The number of items to return can't be negative.");
+ checkArgument(skipCount >= 0, "The number of items to skip can't be negative.");
+ final Page extends Organization> organizationsPage =
+ organizationManager.getByParent(parent, maxItems, skipCount);
+ return Response.ok()
+ .entity(
+ organizationsPage.getItems(
+ organization ->
+ linksInjector.injectLinks(asDto(organization), getServiceContext())))
+ .header("Link", createLinkHeader(organizationsPage))
+ .build();
+ }
+
+ @GET
+ @Produces(APPLICATION_JSON)
+ @Operation(
+ summary =
+ "Get user's organizations. When user parameter is missed then will be fetched current user's organizations",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The organizations successfully fetched",
+ content =
+ @Content(
+ array =
+ @ArraySchema(schema = @Schema(implementation = OrganizationDto.class)))),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Missed required parameters, parameters are not valid"),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public Response getOrganizations(
+ @Parameter(description = "User id") @QueryParam("user") String userId,
+ @Parameter(description = "Max items") @QueryParam("maxItems") @DefaultValue("30")
+ int maxItems,
+ @Parameter(description = "Skip count") @QueryParam("skipCount") @DefaultValue("0")
+ int skipCount)
+ throws ServerException, BadRequestException {
+
+ checkArgument(maxItems >= 0, "The number of items to return can't be negative.");
+ checkArgument(skipCount >= 0, "The number of items to skip can't be negative.");
+ if (userId == null) {
+ userId = EnvironmentContext.getCurrent().getSubject().getUserId();
+ }
+ final Page extends Organization> organizationsPage =
+ organizationManager.getByMember(userId, maxItems, skipCount);
+ return Response.ok()
+ .entity(
+ organizationsPage.getItems(
+ organization ->
+ linksInjector.injectLinks(asDto(organization), getServiceContext())))
+ .header("Link", createLinkHeader(organizationsPage))
+ .build();
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the calling method.
+ *
+ * @param expression a boolean expression
+ * @param errorMessage the exception message to use if the check fails
+ * @throws BadRequestException if {@code expression} is false
+ */
+ private void checkArgument(boolean expression, String errorMessage) throws BadRequestException {
+ if (!expression) {
+ throw new BadRequestException(errorMessage);
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationValidator.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationValidator.java
new file mode 100644
index 00000000000..b4bdbfdc50c
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/OrganizationValidator.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+import javax.inject.Inject;
+import org.eclipse.che.account.spi.AccountValidator;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+
+/**
+ * Utils for organization validation.
+ *
+ * @author Sergii Leschenko
+ */
+public class OrganizationValidator {
+ @Inject private AccountValidator accountValidator;
+
+ /**
+ * Checks whether given organization is valid.
+ *
+ * @param organization organization to check
+ * @throws BadRequestException when organization is not valid
+ */
+ public void checkOrganization(Organization organization) throws BadRequestException {
+ if (organization == null) {
+ throw new BadRequestException("Organization required");
+ }
+ if (isNullOrEmpty(organization.getName())) {
+ throw new BadRequestException("Organization name required");
+ }
+ if (!accountValidator.isValidName(organization.getName())) {
+ throw new BadRequestException(
+ "Organization name may only contain alphanumeric characters or single hyphens inside");
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/event/MemberAddedEvent.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/event/MemberAddedEvent.java
new file mode 100644
index 00000000000..fe9d52ca39f
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/event/MemberAddedEvent.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.event;
+
+import static org.eclipse.che.multiuser.organization.shared.event.EventType.MEMBER_ADDED;
+
+import org.eclipse.che.api.core.model.user.User;
+import org.eclipse.che.multiuser.organization.shared.event.EventType;
+import org.eclipse.che.multiuser.organization.shared.event.MemberEvent;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+
+/**
+ * Defines the event of adding the organization member.
+ *
+ * @author Anton Korneta
+ */
+public class MemberAddedEvent implements MemberEvent {
+
+ private final String initiator;
+ private final User member;
+ private final Organization organization;
+
+ public MemberAddedEvent(String initiator, User member, Organization organization) {
+ this.initiator = initiator;
+ this.member = member;
+ this.organization = organization;
+ }
+
+ @Override
+ public Organization getOrganization() {
+ return organization;
+ }
+
+ @Override
+ public EventType getType() {
+ return MEMBER_ADDED;
+ }
+
+ @Override
+ public String getInitiator() {
+ return initiator;
+ }
+
+ @Override
+ public User getMember() {
+ return member;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/event/MemberRemovedEvent.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/event/MemberRemovedEvent.java
new file mode 100644
index 00000000000..fddbf23b166
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/event/MemberRemovedEvent.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.event;
+
+import org.eclipse.che.api.core.model.user.User;
+import org.eclipse.che.multiuser.organization.shared.event.EventType;
+import org.eclipse.che.multiuser.organization.shared.event.MemberEvent;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+
+/**
+ * Defines the event for organization member removal.
+ *
+ * @author Anton Korneta
+ */
+public class MemberRemovedEvent implements MemberEvent {
+
+ private final String initiator;
+ private final User member;
+ private final Organization organization;
+
+ public MemberRemovedEvent(String initiator, User member, Organization organization) {
+ this.initiator = initiator;
+ this.member = member;
+ this.organization = organization;
+ }
+
+ @Override
+ public EventType getType() {
+ return EventType.MEMBER_REMOVED;
+ }
+
+ @Override
+ public Organization getOrganization() {
+ return organization;
+ }
+
+ @Override
+ public User getMember() {
+ return member;
+ }
+
+ @Override
+ public String getInitiator() {
+ return initiator;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/event/OrganizationRemovedEvent.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/event/OrganizationRemovedEvent.java
new file mode 100644
index 00000000000..dee94e20b28
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/event/OrganizationRemovedEvent.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.event;
+
+import static org.eclipse.che.multiuser.organization.shared.event.EventType.ORGANIZATION_REMOVED;
+
+import java.util.List;
+import org.eclipse.che.multiuser.organization.shared.event.EventType;
+import org.eclipse.che.multiuser.organization.shared.event.OrganizationEvent;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+
+/**
+ * Defines organization removed event.
+ *
+ * @author Anton Korneta
+ */
+public class OrganizationRemovedEvent implements OrganizationEvent {
+
+ private final String initiator;
+ private final Organization organization;
+ private final List members;
+
+ public OrganizationRemovedEvent(
+ String initiator, Organization organization, List members) {
+ this.initiator = initiator;
+ this.organization = organization;
+ this.members = members;
+ }
+
+ @Override
+ public EventType getType() {
+ return ORGANIZATION_REMOVED;
+ }
+
+ @Override
+ public Organization getOrganization() {
+ return organization;
+ }
+
+ public List getMembers() {
+ return members;
+ }
+
+ /** Returns name of user who initiated organization removal */
+ @Override
+ public String getInitiator() {
+ return initiator;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/event/OrganizationRenamedEvent.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/event/OrganizationRenamedEvent.java
new file mode 100644
index 00000000000..0f60ff7afce
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/event/OrganizationRenamedEvent.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.event;
+
+import static org.eclipse.che.multiuser.organization.shared.event.EventType.ORGANIZATION_RENAMED;
+
+import org.eclipse.che.multiuser.organization.shared.event.EventType;
+import org.eclipse.che.multiuser.organization.shared.event.OrganizationEvent;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+
+/**
+ * Defines organization renamed event.
+ *
+ * @author Anton Korneta
+ */
+public class OrganizationRenamedEvent implements OrganizationEvent {
+
+ private final String initiator;
+ private final String oldName;
+ private final String newName;
+ private final Organization organization;
+
+ public OrganizationRenamedEvent(
+ String initiator, String oldName, String newName, Organization organization) {
+ this.initiator = initiator;
+ this.oldName = oldName;
+ this.newName = newName;
+ this.organization = organization;
+ }
+
+ @Override
+ public Organization getOrganization() {
+ return organization;
+ }
+
+ @Override
+ public EventType getType() {
+ return ORGANIZATION_RENAMED;
+ }
+
+ public String getOldName() {
+ return oldName;
+ }
+
+ public String getNewName() {
+ return newName;
+ }
+
+ @Override
+ public String getInitiator() {
+ return initiator;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/listener/MemberEventsPublisher.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/listener/MemberEventsPublisher.java
new file mode 100644
index 00000000000..53e6ba76e6a
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/listener/MemberEventsPublisher.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.listener;
+
+import static org.eclipse.che.multiuser.organization.api.DtoConverter.asDto;
+
+import jakarta.annotation.PostConstruct;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.model.user.User;
+import org.eclipse.che.api.core.notification.EventService;
+import org.eclipse.che.api.core.notification.EventSubscriber;
+import org.eclipse.che.api.user.server.UserManager;
+import org.eclipse.che.multiuser.api.permission.shared.event.PermissionsEvent;
+import org.eclipse.che.multiuser.api.permission.shared.model.Permissions;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.api.event.MemberAddedEvent;
+import org.eclipse.che.multiuser.organization.api.event.MemberRemovedEvent;
+import org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+
+/**
+ * Maps permissions to organization related events.
+ *
+ * @author Anton Korneta
+ */
+@Singleton
+public class MemberEventsPublisher implements EventSubscriber {
+
+ private final EventService eventService;
+ private final UserManager userManager;
+ private final OrganizationManager organizationManager;
+
+ @Inject
+ public MemberEventsPublisher(
+ EventService eventService, UserManager userManager, OrganizationManager organizationManager) {
+ this.eventService = eventService;
+ this.userManager = userManager;
+ this.organizationManager = organizationManager;
+ }
+
+ @PostConstruct
+ private void subscribe() {
+ eventService.subscribe(this);
+ }
+
+ @Override
+ public void onEvent(PermissionsEvent event) {
+ final Permissions permissions = event.getPermissions();
+ if (OrganizationDomain.DOMAIN_ID.equals(permissions.getDomainId())) {
+ try {
+ switch (event.getType()) {
+ case PERMISSIONS_ADDED:
+ {
+ final String initiator = event.getInitiator();
+ final User addedMember = userManager.getById(permissions.getUserId());
+ final Organization org = organizationManager.getById(permissions.getInstanceId());
+ eventService.publish(asDto(new MemberAddedEvent(initiator, addedMember, org)));
+ break;
+ }
+ case PERMISSIONS_REMOVED:
+ {
+ final String initiator = event.getInitiator();
+ final User removedMember = userManager.getById(permissions.getUserId());
+ final Organization org = organizationManager.getById(permissions.getInstanceId());
+ eventService.publish(asDto(new MemberRemovedEvent(initiator, removedMember, org)));
+ break;
+ }
+ default:
+ // do nothing
+ }
+ } catch (NotFoundException | ServerException ignored) {
+ }
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/listener/OrganizationEventsWebsocketBroadcaster.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/listener/OrganizationEventsWebsocketBroadcaster.java
new file mode 100644
index 00000000000..9385b579483
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/listener/OrganizationEventsWebsocketBroadcaster.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.listener;
+
+import static org.eclipse.che.multiuser.organization.shared.event.EventType.MEMBER_ADDED;
+import static org.eclipse.che.multiuser.organization.shared.event.EventType.MEMBER_REMOVED;
+
+import jakarta.annotation.PostConstruct;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.notification.RemoteSubscriptionManager;
+import org.eclipse.che.multiuser.organization.shared.dto.MemberAddedEventDto;
+import org.eclipse.che.multiuser.organization.shared.dto.MemberRemovedEventDto;
+import org.eclipse.che.multiuser.organization.shared.event.OrganizationEvent;
+
+/**
+ * Broadcasts organization events through websocket connection.
+ *
+ * @author Anton Korneta
+ */
+@Singleton
+public class OrganizationEventsWebsocketBroadcaster {
+
+ private final RemoteSubscriptionManager remoteSubscriptionManager;
+
+ public static final String ORGANIZATION_MEMBERSHIP_METHOD_NAME = "organization/membershipChanged";
+ public static final String ORGANIZATION_CHANGED_METHOD_NAME = "organization/statusChanged";
+
+ @Inject
+ public OrganizationEventsWebsocketBroadcaster(
+ RemoteSubscriptionManager remoteSubscriptionManager) {
+ this.remoteSubscriptionManager = remoteSubscriptionManager;
+ }
+
+ @PostConstruct
+ private void subscribe() {
+ remoteSubscriptionManager.register(
+ ORGANIZATION_MEMBERSHIP_METHOD_NAME, OrganizationEvent.class, this::predicate);
+ remoteSubscriptionManager.register(
+ ORGANIZATION_CHANGED_METHOD_NAME, OrganizationEvent.class, this::predicate);
+ }
+
+ private boolean predicate(OrganizationEvent event, Map scope) {
+ if (MEMBER_ADDED == event.getType()) {
+ return ((MemberAddedEventDto) event).getMember().getId().equals(scope.get("userId"));
+ } else if (MEMBER_REMOVED == event.getType()) {
+ return ((MemberRemovedEventDto) event).getMember().getId().equals(scope.get("userId"));
+ } else {
+ return event.getOrganization().getId().equals(scope.get("organizationId"));
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationDomain.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationDomain.java
new file mode 100644
index 00000000000..d4fd34fa34f
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationDomain.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.permissions;
+
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+import org.eclipse.che.multiuser.api.permission.server.AbstractPermissionsDomain;
+import org.eclipse.che.multiuser.organization.spi.impl.MemberImpl;
+
+/**
+ * Domain for storing organizations' permissions
+ *
+ * @author Sergii Leschenko
+ */
+public class OrganizationDomain extends AbstractPermissionsDomain {
+ public static final String DOMAIN_ID = "organization";
+
+ public static final String UPDATE = "update";
+ public static final String DELETE = "delete";
+ public static final String MANAGE_SUBORGANIZATIONS = "manageSuborganizations";
+ public static final String MANAGE_RESOURCES = "manageResources";
+ public static final String CREATE_WORKSPACES = "createWorkspaces";
+ public static final String MANAGE_WORKSPACES = "manageWorkspaces";
+
+ private static final List ACTIONS =
+ ImmutableList.of(
+ SET_PERMISSIONS,
+ UPDATE,
+ DELETE,
+ MANAGE_SUBORGANIZATIONS,
+ MANAGE_RESOURCES,
+ CREATE_WORKSPACES,
+ MANAGE_WORKSPACES);
+
+ /** Returns all the available actions for {@link OrganizationDomain}. */
+ public static List getActions() {
+ return ACTIONS;
+ }
+
+ public OrganizationDomain() {
+ super(DOMAIN_ID, ACTIONS);
+ }
+
+ @Override
+ protected MemberImpl doCreateInstance(
+ String userId, String instanceId, List allowedActions) {
+ return new MemberImpl(userId, instanceId, allowedActions);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationPermissionsFilter.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationPermissionsFilter.java
new file mode 100644
index 00000000000..7ec0088ba33
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationPermissionsFilter.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.permissions;
+
+import static org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain.DOMAIN_ID;
+import static org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain.MANAGE_SUBORGANIZATIONS;
+
+import jakarta.ws.rs.Path;
+import javax.inject.Inject;
+import org.eclipse.che.api.core.ApiException;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.everrest.CheMethodInvokerFilter;
+import org.eclipse.che.multiuser.api.permission.server.SuperPrivilegesChecker;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.api.OrganizationService;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationDto;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+import org.everrest.core.Filter;
+import org.everrest.core.resource.GenericResourceMethod;
+
+/**
+ * Restricts access to methods of {@link OrganizationService} by users' permissions
+ *
+ *
Filter contains rules for protecting of all methods of {@link OrganizationService}.
+ * In case when requested method is unknown filter throws {@link ForbiddenException}
+ *
+ * @author Sergii Leschenko
+ */
+@Filter
+@Path("/organization{path:(?!/resource)(/.*)?}")
+public class OrganizationPermissionsFilter extends CheMethodInvokerFilter {
+ static final String CREATE_METHOD = "create";
+ static final String UPDATE_METHOD = "update";
+ static final String REMOVE_METHOD = "remove";
+ static final String GET_BY_PARENT_METHOD = "getByParent";
+ static final String GET_ORGANIZATIONS_METHOD = "getOrganizations";
+ static final String GET_BY_ID_METHOD = "getById";
+ static final String FIND_METHOD = "find";
+
+ @Inject private OrganizationManager manager;
+ @Inject private SuperPrivilegesChecker superPrivilegesChecker;
+
+ @Override
+ protected void filter(GenericResourceMethod genericMethodResource, Object[] arguments)
+ throws ApiException {
+ final String methodName = genericMethodResource.getMethod().getName();
+
+ final Subject currentSubject = EnvironmentContext.getCurrent().getSubject();
+ String action;
+ String organizationId;
+
+ switch (methodName) {
+ case CREATE_METHOD:
+ final OrganizationDto organization = (OrganizationDto) arguments[0];
+ if (organization.getParent() != null) {
+ organizationId = organization.getParent();
+ action = OrganizationDomain.MANAGE_SUBORGANIZATIONS;
+ break;
+ }
+ // anybody can create root organization
+ return;
+
+ case UPDATE_METHOD:
+ organizationId = ((String) arguments[0]);
+ action = OrganizationDomain.UPDATE;
+ break;
+
+ case REMOVE_METHOD:
+ organizationId = ((String) arguments[0]);
+ action = OrganizationDomain.DELETE;
+ break;
+
+ case GET_BY_PARENT_METHOD:
+ organizationId = ((String) arguments[0]);
+ action = OrganizationDomain.MANAGE_SUBORGANIZATIONS;
+ if (superPrivilegesChecker.hasSuperPrivileges()) {
+ return;
+ }
+ break;
+
+ case GET_ORGANIZATIONS_METHOD:
+ final String userId = (String) arguments[0];
+ if (userId != null
+ && !userId.equals(currentSubject.getUserId())
+ && !superPrivilegesChecker.hasSuperPrivileges()) {
+ throw new ForbiddenException("The user is able to specify only his own id");
+ }
+ // user specified his user id or has super privileges
+ return;
+
+ // methods accessible to every user
+ case GET_BY_ID_METHOD:
+ case FIND_METHOD:
+ return;
+
+ default:
+ throw new ForbiddenException("The user does not have permission to perform this operation");
+ }
+
+ // user is not admin and it is need to check permissions on organization instance level
+ final Organization organization = manager.getById(organizationId);
+ final String parentOrganizationId = organization.getParent();
+ // check permissions on parent organization level when updating or removing child organization
+ if (parentOrganizationId != null
+ && (OrganizationDomain.UPDATE.equals(action) || OrganizationDomain.DELETE.equals(action))) {
+ if (currentSubject.hasPermission(
+ OrganizationDomain.DOMAIN_ID, parentOrganizationId, MANAGE_SUBORGANIZATIONS)) {
+ // user has permissions to manage organization on parent organization level
+ return;
+ }
+ }
+
+ if (!currentSubject.hasPermission(DOMAIN_ID, organizationId, action)) {
+ throw new ForbiddenException(
+ "The user does not have permission to "
+ + action
+ + " organization with id '"
+ + organizationId
+ + "'");
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationRemoteSubscriptionPermissionsChecks.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationRemoteSubscriptionPermissionsChecks.java
new file mode 100644
index 00000000000..1e41e2ae9d4
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationRemoteSubscriptionPermissionsChecks.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.permissions;
+
+import static org.eclipse.che.multiuser.organization.api.listener.OrganizationEventsWebsocketBroadcaster.ORGANIZATION_CHANGED_METHOD_NAME;
+import static org.eclipse.che.multiuser.organization.api.listener.OrganizationEventsWebsocketBroadcaster.ORGANIZATION_MEMBERSHIP_METHOD_NAME;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.multiuser.api.permission.server.PermissionsManager;
+import org.eclipse.che.multiuser.api.permission.server.jsonrpc.RemoteSubscriptionPermissionCheck;
+import org.eclipse.che.multiuser.api.permission.server.jsonrpc.RemoteSubscriptionPermissionManager;
+import org.eclipse.che.multiuser.api.permission.server.model.impl.AbstractPermissions;
+import org.eclipse.che.multiuser.organization.api.listener.OrganizationEventsWebsocketBroadcaster;
+
+/**
+ * Holds and registers permissions checks for organization related events.
+ *
+ *
Covers events published via {@link OrganizationEventsWebsocketBroadcaster}.
+ *
+ * @author Sergii Leshchenko
+ */
+@Singleton
+public class OrganizationRemoteSubscriptionPermissionsChecks {
+
+ private final PermissionsManager permissionsManager;
+
+ @Inject
+ public OrganizationRemoteSubscriptionPermissionsChecks(PermissionsManager permissionsManager) {
+ this.permissionsManager = permissionsManager;
+ }
+
+ @Inject
+ public void register(RemoteSubscriptionPermissionManager permissionFilter) {
+ MembershipsChangedSubscriptionCheck membershipsEventsCheck =
+ new MembershipsChangedSubscriptionCheck();
+
+ permissionFilter.registerCheck(membershipsEventsCheck, ORGANIZATION_MEMBERSHIP_METHOD_NAME);
+
+ OrganizationChangedSubscriptionCheck organizationChangedCheck =
+ new OrganizationChangedSubscriptionCheck(permissionsManager);
+ permissionFilter.registerCheck(organizationChangedCheck, ORGANIZATION_CHANGED_METHOD_NAME);
+ }
+
+ @VisibleForTesting
+ static class MembershipsChangedSubscriptionCheck implements RemoteSubscriptionPermissionCheck {
+
+ @Override
+ public void check(String methodName, Map scope) throws ForbiddenException {
+ String userId = scope.get("userId");
+ if (userId == null) {
+ throw new ForbiddenException("User id must be specified in scope");
+ }
+
+ String currentUserId = EnvironmentContext.getCurrent().getSubject().getUserId();
+
+ if (!currentUserId.equals(userId)) {
+ throw new ForbiddenException("It is only allowed to listen to own memberships changes");
+ }
+ }
+ }
+
+ @VisibleForTesting
+ static class OrganizationChangedSubscriptionCheck implements RemoteSubscriptionPermissionCheck {
+
+ private final PermissionsManager permissionsManager;
+
+ public OrganizationChangedSubscriptionCheck(PermissionsManager permissionsManager) {
+ this.permissionsManager = permissionsManager;
+ }
+
+ @Override
+ public void check(String methodName, Map scope) throws ForbiddenException {
+ String organizationId = scope.get("organizationId");
+ if (organizationId == null) {
+ throw new ForbiddenException("Organization id must be specified in scope");
+ }
+
+ String currentUserId = EnvironmentContext.getCurrent().getSubject().getUserId();
+
+ try {
+ // check if user has any permissions in organisation
+ // to listen to related events
+ AbstractPermissions permissions =
+ permissionsManager.get(currentUserId, OrganizationDomain.DOMAIN_ID, organizationId);
+ } catch (ConflictException | ServerException e) {
+ throw new ForbiddenException("Error occurred while permission fetching: " + e.getMessage());
+ } catch (NotFoundException e) {
+ throw new ForbiddenException(
+ "User doesn't have any permissions for the specified organization");
+ }
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationResourceDistributionServicePermissionsFilter.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationResourceDistributionServicePermissionsFilter.java
new file mode 100644
index 00000000000..3d167626444
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationResourceDistributionServicePermissionsFilter.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.permissions;
+
+import jakarta.ws.rs.Path;
+import javax.inject.Inject;
+import org.eclipse.che.api.core.ApiException;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.everrest.CheMethodInvokerFilter;
+import org.eclipse.che.multiuser.api.permission.server.SuperPrivilegesChecker;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.api.resource.OrganizationResourcesDistributionService;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+import org.everrest.core.Filter;
+import org.everrest.core.resource.GenericResourceMethod;
+
+/**
+ * Restricts access to methods of {@link OrganizationResourcesDistributionService} by users'
+ * permissions.
+ *
+ *
Filter contains rules for protecting of all methods of {@link
+ * OrganizationResourcesDistributionService}.
+ * In case when requested method is unknown filter throws {@link ForbiddenException}.
+ *
+ * @author Sergii Leschenko
+ */
+@Filter
+@Path("/organization/resource{path:(/.*)?}")
+public class OrganizationResourceDistributionServicePermissionsFilter
+ extends CheMethodInvokerFilter {
+ static final String CAP_RESOURCES_METHOD = "capResources";
+ static final String GET_RESOURCES_CAP_METHOD = "getResourcesCap";
+ static final String GET_DISTRIBUTED_RESOURCES = "getDistributedResources";
+
+ @Inject private OrganizationManager organizationManager;
+ @Inject private SuperPrivilegesChecker superPrivilegesChecker;
+
+ @Override
+ protected void filter(GenericResourceMethod genericMethodResource, Object[] arguments)
+ throws ApiException {
+ final String methodName = genericMethodResource.getMethod().getName();
+
+ final Subject currentSubject = EnvironmentContext.getCurrent().getSubject();
+ String organizationId;
+ switch (methodName) {
+ case GET_RESOURCES_CAP_METHOD:
+ if (superPrivilegesChecker.hasSuperPrivileges()) {
+ // user is able to see information about all organizations
+ return;
+ }
+ // fall through
+ case CAP_RESOURCES_METHOD:
+ // we should check permissions on parent organization level
+ Organization organization = organizationManager.getById((String) arguments[0]);
+ organizationId = organization.getParent();
+ if (organizationId == null) {
+ // requested organization is root so manager should throw exception
+ return;
+ }
+ break;
+
+ case GET_DISTRIBUTED_RESOURCES:
+ organizationId = (String) arguments[0];
+ // get organization to ensure that organization exists
+ organizationManager.getById(organizationId);
+ if (superPrivilegesChecker.hasSuperPrivileges()) {
+ // user is able to see information about all organizations
+ return;
+ }
+ break;
+
+ default:
+ throw new ForbiddenException("The user does not have permission to perform this operation");
+ }
+
+ if (!currentSubject.hasPermission(
+ OrganizationDomain.DOMAIN_ID, organizationId, OrganizationDomain.MANAGE_RESOURCES)) {
+ throw new ForbiddenException(
+ "The user does not have permission to manage resources of organization with id '"
+ + organizationId
+ + "'");
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationalAccountPermissionsChecker.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationalAccountPermissionsChecker.java
new file mode 100644
index 00000000000..a6a0a593d29
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationalAccountPermissionsChecker.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.permissions;
+
+import static org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain.CREATE_WORKSPACES;
+import static org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain.DOMAIN_ID;
+import static org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain.MANAGE_RESOURCES;
+import static org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain.MANAGE_WORKSPACES;
+
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.multiuser.api.permission.server.account.AccountOperation;
+import org.eclipse.che.multiuser.api.permission.server.account.AccountPermissionsChecker;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+
+/**
+ * Defines permissions checking for organizational accounts.
+ *
+ * @author Sergii Leshchenko
+ */
+@Singleton
+public class OrganizationalAccountPermissionsChecker implements AccountPermissionsChecker {
+ @Override
+ public void checkPermissions(String accountId, AccountOperation operation)
+ throws ForbiddenException {
+ Subject subject = EnvironmentContext.getCurrent().getSubject();
+ switch (operation) {
+ case CREATE_WORKSPACE:
+ if (!subject.hasPermission(
+ OrganizationDomain.DOMAIN_ID, accountId, OrganizationDomain.CREATE_WORKSPACES)) {
+ throw new ForbiddenException(
+ "User is not authorized to create workspaces in specified namespace.");
+ }
+ break;
+ case MANAGE_WORKSPACES:
+ if (!subject.hasPermission(
+ OrganizationDomain.DOMAIN_ID, accountId, OrganizationDomain.MANAGE_WORKSPACES)) {
+ throw new ForbiddenException("User is not authorized to use specified namespace.");
+ }
+ break;
+ case SEE_RESOURCE_INFORMATION:
+ if (subject.hasPermission(DOMAIN_ID, accountId, CREATE_WORKSPACES)
+ || subject.hasPermission(DOMAIN_ID, accountId, MANAGE_WORKSPACES)
+ || subject.hasPermission(DOMAIN_ID, accountId, MANAGE_RESOURCES)) {
+
+ // user is able to see resources usage information
+ return;
+ }
+ throw new ForbiddenException(
+ "User is not authorized to see resources information of requested organization.");
+ default:
+ throw new ForbiddenException("User is not authorized to use specified namespace.");
+ }
+ }
+
+ @Override
+ public String getAccountType() {
+ return OrganizationImpl.ORGANIZATIONAL_ACCOUNT;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/DefaultOrganizationResourcesProvider.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/DefaultOrganizationResourcesProvider.java
new file mode 100644
index 00000000000..ffc287bc6b4
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/DefaultOrganizationResourcesProvider.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.resource;
+
+import static java.util.Arrays.asList;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import javax.inject.Inject;
+import javax.inject.Named;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.commons.lang.Size;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+import org.eclipse.che.multiuser.resource.api.free.DefaultResourcesProvider;
+import org.eclipse.che.multiuser.resource.api.type.RamResourceType;
+import org.eclipse.che.multiuser.resource.api.type.RuntimeResourceType;
+import org.eclipse.che.multiuser.resource.api.type.TimeoutResourceType;
+import org.eclipse.che.multiuser.resource.api.type.WorkspaceResourceType;
+import org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl;
+
+/**
+ * Provided free resources that are available for usage by organizational accounts by default.
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class DefaultOrganizationResourcesProvider implements DefaultResourcesProvider {
+ private final OrganizationManager organizationManager;
+ private final long ramPerOrganization;
+ private final int workspacesPerOrganization;
+ private final int runtimesPerOrganization;
+ private final long timeout;
+
+ @Inject
+ public DefaultOrganizationResourcesProvider(
+ OrganizationManager organizationManager,
+ @Named("che.limits.organization.workspaces.ram") String ramPerOrganization,
+ @Named("che.limits.organization.workspaces.count") int workspacesPerOrganization,
+ @Named("che.limits.organization.workspaces.run.count") int runtimesPerOrganization,
+ @Named("che.limits.workspace.idle.timeout") long timeout) {
+ this.timeout = TimeUnit.MILLISECONDS.toMinutes(timeout);
+ this.organizationManager = organizationManager;
+ this.ramPerOrganization =
+ "-1".equals(ramPerOrganization) ? -1 : Size.parseSizeToMegabytes(ramPerOrganization);
+ this.workspacesPerOrganization = workspacesPerOrganization;
+ this.runtimesPerOrganization = runtimesPerOrganization;
+ }
+
+ @Override
+ public String getAccountType() {
+ return OrganizationImpl.ORGANIZATIONAL_ACCOUNT;
+ }
+
+ @Override
+ public List getResources(String accountId)
+ throws ServerException, NotFoundException {
+ final Organization organization = organizationManager.getById(accountId);
+ // only root organizations should have own resources
+ if (organization.getParent() == null) {
+ return asList(
+ new ResourceImpl(TimeoutResourceType.ID, timeout, TimeoutResourceType.UNIT),
+ new ResourceImpl(RamResourceType.ID, ramPerOrganization, RamResourceType.UNIT),
+ new ResourceImpl(
+ WorkspaceResourceType.ID, workspacesPerOrganization, WorkspaceResourceType.UNIT),
+ new ResourceImpl(
+ RuntimeResourceType.ID, runtimesPerOrganization, RuntimeResourceType.UNIT));
+ }
+
+ return Collections.emptyList();
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourceLockKeyProvider.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourceLockKeyProvider.java
new file mode 100644
index 00000000000..bbcd871f898
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourceLockKeyProvider.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.resource;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+import org.eclipse.che.multiuser.resource.api.ResourceLockKeyProvider;
+
+/**
+ * Provides resources lock key for accounts with organizational type.
+ *
+ *
A lock key for any organization is an identifier of the root organization.
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class OrganizationResourceLockKeyProvider implements ResourceLockKeyProvider {
+ private final OrganizationManager organizationManager;
+
+ @Inject
+ public OrganizationResourceLockKeyProvider(OrganizationManager organizationManager) {
+ this.organizationManager = organizationManager;
+ }
+
+ @Override
+ public String getLockKey(String accountId) throws ServerException {
+ String currentOrganizationId = accountId;
+ try {
+ Organization organization = organizationManager.getById(currentOrganizationId);
+ while (organization.getParent() != null) {
+ currentOrganizationId = organization.getParent();
+ organization = organizationManager.getById(currentOrganizationId);
+ }
+ return organization.getId();
+ } catch (NotFoundException e) {
+ // should not happen
+ throw new ServerException(e.getLocalizedMessage(), e);
+ }
+ }
+
+ @Override
+ public String getAccountType() {
+ return OrganizationImpl.ORGANIZATIONAL_ACCOUNT;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourcesDistributionService.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourcesDistributionService.java
new file mode 100644
index 00000000000..8d0bae84b0b
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourcesDistributionService.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.resource;
+
+import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
+import static java.lang.String.format;
+import static java.util.stream.Collectors.toList;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.Response;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import javax.inject.Inject;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.rest.Service;
+import org.eclipse.che.multiuser.organization.api.DtoConverter;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationDistributedResourcesDto;
+import org.eclipse.che.multiuser.organization.shared.model.OrganizationDistributedResources;
+import org.eclipse.che.multiuser.resource.api.free.ResourceValidator;
+import org.eclipse.che.multiuser.resource.shared.dto.ResourceDto;
+
+/**
+ * REST API for resources distribution between suborganizations.
+ *
+ * @author Sergii Leschenko
+ */
+@Tag(
+ name = "organization-resource",
+ description = "REST API for resources distribution between suborganizations")
+@Path("/organization/resource")
+public class OrganizationResourcesDistributionService extends Service {
+ private final OrganizationResourcesDistributor resourcesDistributor;
+ private final ResourceValidator resourceValidator;
+
+ @Inject
+ public OrganizationResourcesDistributionService(
+ OrganizationResourcesDistributor resourcesDistributor, ResourceValidator resourceValidator) {
+ this.resourcesDistributor = resourcesDistributor;
+ this.resourceValidator = resourceValidator;
+ }
+
+ @POST
+ @Path("/{suborganizationId}/cap")
+ @Consumes(APPLICATION_JSON)
+ @Operation(
+ summary =
+ "Cap usage of shared resources.. By default suborganization is able to use all parent organization resources."
+ + "Cap allow to limit usage of shared resources by suborganization.",
+ responses = {
+ @ApiResponse(responseCode = "204", description = "Resources successfully capped"),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Missed required parameters, parameters are not valid"),
+ @ApiResponse(responseCode = "404", description = "Specified organization was not found"),
+ @ApiResponse(
+ responseCode = "409",
+ description = "Specified organization is root organization"),
+ @ApiResponse(
+ responseCode = "409",
+ description = "Suborganization is using shared resources"),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public void capResources(
+ @Parameter(description = "Suborganization id") @PathParam("suborganizationId")
+ String suborganizationId,
+ @Parameter(description = "Resources to cap") List resourcesCap)
+ throws BadRequestException, NotFoundException, ConflictException, ServerException {
+ checkArgument(resourcesCap != null, "Missed resources caps.");
+ Set resourcesToSet = new HashSet<>();
+ for (ResourceDto resource : resourcesCap) {
+ if (!resourcesToSet.add(resource.getType())) {
+ throw new BadRequestException(
+ format(
+ "Resources to cap must contain only one resource with type '%s'.",
+ resource.getType()));
+ }
+ resourceValidator.validate(resource);
+ }
+
+ resourcesDistributor.capResources(suborganizationId, resourcesCap);
+ }
+
+ @GET
+ @Path("/{suborganizationId}/cap")
+ @Produces(APPLICATION_JSON)
+ @Operation(
+ summary = "Get resources cap of specified suborganization.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Resources caps successfully fetched",
+ content =
+ @Content(
+ array =
+ @ArraySchema(
+ schema =
+ @Schema(
+ implementation = OrganizationDistributedResourcesDto.class)))),
+ @ApiResponse(responseCode = "404", description = "Specified organization was not found"),
+ @ApiResponse(
+ responseCode = "409",
+ description = "Specified organization is root organization"),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public List getResourcesCap(
+ @Parameter(description = "Suborganization id") @PathParam("suborganizationId")
+ String suborganization)
+ throws NotFoundException, ConflictException, ServerException {
+ return resourcesDistributor.getResourcesCaps(suborganization).stream()
+ .map(org.eclipse.che.multiuser.resource.api.DtoConverter::asDto)
+ .collect(toList());
+ }
+
+ @GET
+ @Path("/{organizationId}")
+ @Produces(APPLICATION_JSON)
+ @Operation(
+ summary = "Get resources which are distributed by specified parent.",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "Resources caps successfully fetched",
+ content =
+ @Content(
+ array =
+ @ArraySchema(
+ schema =
+ @Schema(
+ implementation = OrganizationDistributedResourcesDto.class)))),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public Response getDistributedResources(
+ @Parameter(description = "Organization id") @PathParam("organizationId")
+ String organizationId,
+ @Parameter(description = "Max items") @QueryParam("maxItems") @DefaultValue("30")
+ int maxItems,
+ @Parameter(description = "Skip count") @QueryParam("skipCount") @DefaultValue("0")
+ long skipCount)
+ throws BadRequestException, ServerException {
+ checkArgument(maxItems >= 0, "The number of items to return can't be negative.");
+ checkArgument(skipCount >= 0, "The number of items to skip can't be negative.");
+
+ final Page extends OrganizationDistributedResources> distributedResourcesPage =
+ resourcesDistributor.getByParent(organizationId, maxItems, skipCount);
+ return Response.ok()
+ .entity(distributedResourcesPage.getItems(DtoConverter::asDto))
+ .header("Link", createLinkHeader(distributedResourcesPage))
+ .build();
+ }
+
+ /**
+ * Ensures the truth of an expression involving one or more parameters to the calling method.
+ *
+ * @param expression a boolean expression
+ * @param errorMessage the exception message to use if the check fails
+ * @throws BadRequestException if {@code expression} is false
+ */
+ private void checkArgument(boolean expression, String errorMessage) throws BadRequestException {
+ if (!expression) {
+ throw new BadRequestException(errorMessage);
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourcesDistributor.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourcesDistributor.java
new file mode 100644
index 00000000000..7a7c2a6cdf5
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourcesDistributor.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.resource;
+
+import static java.util.Collections.emptyList;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toList;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.commons.lang.concurrent.Unlocker;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.shared.model.OrganizationDistributedResources;
+import org.eclipse.che.multiuser.organization.spi.OrganizationDistributedResourcesDao;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationDistributedResourcesImpl;
+import org.eclipse.che.multiuser.resource.api.ResourceAggregator;
+import org.eclipse.che.multiuser.resource.api.exception.NoEnoughResourcesException;
+import org.eclipse.che.multiuser.resource.api.type.RamResourceType;
+import org.eclipse.che.multiuser.resource.api.type.RuntimeResourceType;
+import org.eclipse.che.multiuser.resource.api.type.WorkspaceResourceType;
+import org.eclipse.che.multiuser.resource.api.usage.ResourceManager;
+import org.eclipse.che.multiuser.resource.api.usage.ResourcesLocks;
+import org.eclipse.che.multiuser.resource.model.Resource;
+
+/**
+ * Facade for organization resources operations.
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class OrganizationResourcesDistributor {
+ private final OrganizationDistributedResourcesDao organizationDistributedResourcesDao;
+ private final OrganizationManager organizationManager;
+ private final ResourcesLocks resourcesLocks;
+ private final ResourceManager resourceManager;
+ private final ResourceAggregator resourceAggregator;
+
+ @Inject
+ public OrganizationResourcesDistributor(
+ OrganizationDistributedResourcesDao organizationDistributedResourcesDao,
+ OrganizationManager organizationManager,
+ ResourcesLocks resourcesLocks,
+ ResourceManager resourceManager,
+ ResourceAggregator resourceAggregator) {
+ this.organizationDistributedResourcesDao = organizationDistributedResourcesDao;
+ this.organizationManager = organizationManager;
+ this.resourcesLocks = resourcesLocks;
+ this.resourceManager = resourceManager;
+ this.resourceAggregator = resourceAggregator;
+ }
+
+ /**
+ * Cap usage of shared resources.
+ *
+ *
By default suborganization is able to use all parent organization resources Cap allow to
+ * limit usage of shared resources by suborganization.
+ *
+ * @param suborganizationId suborganization id
+ * @param resourcesCaps resources to capped
+ * @throws NotFoundException when specified suborganization was not found
+ * @throws ConflictException when organization with specified id is root organization
+ * @throws ConflictException when suborganization is currently using more shared resources than
+ * should be capped
+ * @throws ServerException when any other error occurs
+ */
+ public void capResources(String suborganizationId, List extends Resource> resourcesCaps)
+ throws NotFoundException, ConflictException, ServerException {
+ requireNonNull(suborganizationId, "Required non-null suborganization id");
+ requireNonNull(resourcesCaps, "Required non-null resources to capResources");
+ checkIsSuborganization(suborganizationId);
+
+ // remove caps with amount -1
+ resourcesCaps = resourcesCaps.stream().filter(res -> res.getAmount() != -1).collect(toList());
+
+ // locking resources by suborganization should lock resources whole organization tree
+ // so we can check resource availability for suborganization organization
+ try (@SuppressWarnings("unused")
+ Unlocker u = resourcesLocks.lock(suborganizationId)) {
+ if (resourcesCaps.isEmpty()) {
+ organizationDistributedResourcesDao.remove(suborganizationId);
+ } else {
+ checkResourcesAvailability(suborganizationId, resourcesCaps);
+
+ organizationDistributedResourcesDao.store(
+ new OrganizationDistributedResourcesImpl(suborganizationId, resourcesCaps));
+ }
+ }
+ }
+
+ /**
+ * Returns resources cap or empty list.
+ *
+ * @param suborganizationId suborganization id to fetch resources cap
+ * @return resources cap or empty list
+ * @throws NotFoundException when specified suborganization was not found
+ * @throws ConflictException when organization with specified id is root organization
+ * @throws ServerException when any other error occurs
+ */
+ public List extends Resource> getResourcesCaps(String suborganizationId)
+ throws NotFoundException, ConflictException, ServerException {
+ requireNonNull(suborganizationId, "Required non-null suborganization id");
+ checkIsSuborganization(suborganizationId);
+ try {
+ return organizationDistributedResourcesDao.get(suborganizationId).getResourcesCap();
+ } catch (NotFoundException e) {
+ return emptyList();
+ }
+ }
+
+ /**
+ * Returns distributed resources for specified suborganization.
+ *
+ * @param suborganizationId organization id
+ * @return distributed resources for suborganization with specified id
+ * @throws NullPointerException when either {@code suborganizationId} is null
+ * @throws NotFoundException when there is not distributed resources for specified suborganization
+ * @throws ServerException when any other error occurs
+ */
+ public OrganizationDistributedResources get(String suborganizationId)
+ throws NotFoundException, ServerException {
+ requireNonNull(suborganizationId, "Required non-null organization id");
+
+ return organizationDistributedResourcesDao.get(suborganizationId);
+ }
+
+ /**
+ * Returns distributed resources for suborganizations by specified parent organization.
+ *
+ * @param organizationId organization id
+ * @return distributed resources for suborganizations by specified parent organization
+ * @throws NullPointerException when either {@code organizationId} is null
+ * @throws ServerException when any other error occurs
+ */
+ public Page extends OrganizationDistributedResources> getByParent(
+ String organizationId, int maxItems, long skipCount) throws ServerException {
+ requireNonNull(organizationId, "Required non-null organization id");
+
+ return organizationDistributedResourcesDao.getByParent(organizationId, maxItems, skipCount);
+ }
+
+ /**
+ * Checks that suborganization is using less resources that new resources cap defines.
+ *
+ * @param suborganizationId identifier of suborganization
+ * @param newResourcesCap resources to capResources
+ * @throws ConflictException when parent organization doesn't have enough resources to increase
+ * distributed resource amount
+ * @throws ConflictException when resources can't be distributed because suborganization is using
+ * existing resources or when they are distributed to next organizations level
+ * @throws ServerException when any other error occurs
+ */
+ @VisibleForTesting
+ void checkResourcesAvailability(
+ String suborganizationId, List extends Resource> newResourcesCap)
+ throws NotFoundException, ConflictException, ServerException {
+ Map usedResources =
+ resourceManager.getUsedResources(suborganizationId).stream()
+ .collect(Collectors.toMap(Resource::getType, Function.identity()));
+ for (Resource resourceToCheck : newResourcesCap) {
+ Resource usedResource = usedResources.get(resourceToCheck.getType());
+ if (usedResource != null) {
+ try {
+ resourceAggregator.deduct(resourceToCheck, usedResource);
+ } catch (NoEnoughResourcesException e) {
+ throw new ConflictException(
+ "Resources are currently in use. "
+ + getMessage(e.getMissingResources().get(0).getType()));
+ }
+ }
+ }
+ }
+
+ @VisibleForTesting
+ String getMessage(String requiredResourceType) {
+ switch (requiredResourceType) {
+ case RamResourceType.ID:
+ return "You can't decrease RAM CAP, while the resources are in use. "
+ + "Free resources, by stopping workspaces, before changing the RAM CAP.";
+ case WorkspaceResourceType.ID:
+ return "You can't reduce the workspaces CAP to a value lower than the number of workspaces currently created. "
+ + "Free resources, by removing workspaces, before changing the workspaces CAP.";
+ case RuntimeResourceType.ID:
+ return "You can't reduce the running workspaces CAP to a value lower than the number of workspaces currently running. "
+ + "Free resources, by stopping workspaces, before changing the running workspaces CAP.";
+ default:
+ return "You can't reduce them while they are used. "
+ + "Free resources before changing the resources CAP.";
+ }
+ }
+
+ private String checkIsSuborganization(String organizationId)
+ throws NotFoundException, ConflictException, ServerException {
+ String parentOrganization = organizationManager.getById(organizationId).getParent();
+ if (parentOrganization == null) {
+ throw new ConflictException("It is not allowed to cap resources for root organization.");
+ }
+ return parentOrganization;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationalAccountAvailableResourcesProvider.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationalAccountAvailableResourcesProvider.java
new file mode 100644
index 00000000000..12cbc5e51fe
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationalAccountAvailableResourcesProvider.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.resource;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Pages;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+import org.eclipse.che.multiuser.resource.api.AvailableResourcesProvider;
+import org.eclipse.che.multiuser.resource.api.ResourceAggregator;
+import org.eclipse.che.multiuser.resource.api.exception.NoEnoughResourcesException;
+import org.eclipse.che.multiuser.resource.api.usage.ResourceManager;
+import org.eclipse.che.multiuser.resource.model.Resource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Provides available resources for organizational and suborganizational accounts.
+ *
+ *
Root organizational account can use resources by itself or share them for its
+ * suborganizations. So available resources equal to total resources minus resources which are
+ * already used by organization or by any of its suborganizations.
+ *
+ *
Suborganizational account can use all of parent resources or limited amount. So available
+ * resource equal to minimum of parent available resources and parent shared resources minus
+ * resources which are used by suborganization and its suborganizations.
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class OrganizationalAccountAvailableResourcesProvider implements AvailableResourcesProvider {
+ private static final Logger LOG =
+ LoggerFactory.getLogger(OrganizationalAccountAvailableResourcesProvider.class);
+
+ private final Provider resourceManagerProvider;
+ private final ResourceAggregator resourceAggregator;
+ private final OrganizationManager organizationManager;
+
+ @Inject
+ public OrganizationalAccountAvailableResourcesProvider(
+ Provider resourceManagerProvider,
+ ResourceAggregator resourceAggregator,
+ OrganizationManager organizationManager) {
+ this.resourceManagerProvider = resourceManagerProvider;
+ this.resourceAggregator = resourceAggregator;
+ this.organizationManager = organizationManager;
+ }
+
+ @Override
+ public List extends Resource> getAvailableResources(String accountId)
+ throws NotFoundException, ServerException {
+ Organization organization = organizationManager.getById(accountId);
+
+ if (organization.getParent() == null) {
+ return getAvailableOrganizationResources(organization);
+ } else {
+ Organization parentOrganization = organizationManager.getById(organization.getParent());
+ return resourceAggregator.min(
+ resourceAggregator.intersection(
+ getAvailableOrganizationResources(parentOrganization),
+ getAvailableOrganizationResources(organization)));
+ }
+ }
+
+ /**
+ * Returns total resources minus resources which are already used by organization or by any of its
+ * suborganizations.
+ *
+ * @param organization organization id to calculate its available resources
+ * @return resources which are available for usage by specified organization
+ * @throws NotFoundException when organization with specified id doesn't exist
+ * @throws ServerException when any other exception occurs on calculation of available resources
+ */
+ @VisibleForTesting
+ List extends Resource> getAvailableOrganizationResources(Organization organization)
+ throws NotFoundException, ServerException {
+ final ResourceManager resourceManager = resourceManagerProvider.get();
+ final List extends Resource> total = resourceManager.getTotalResources(organization.getId());
+ final List unavailable =
+ new ArrayList<>(resourceManager.getUsedResources(organization.getId()));
+ unavailable.addAll(getUsedResourcesBySuborganizations(organization.getQualifiedName()));
+ try {
+ return resourceAggregator.deduct(total, unavailable);
+ } catch (NoEnoughResourcesException e) {
+ LOG.warn(
+ "Organization with id {} uses more resources {} than it has {}.",
+ organization.getId(),
+ format(unavailable),
+ format(total));
+ return resourceAggregator.excess(total, unavailable);
+ }
+ }
+
+ /**
+ * Returns resources which are used by suborganizations of specified organization.
+ *
+ *
Note that the result will includes used resources of all direct and nested suborganizations.
+ *
+ * @param parentQualifiedName parent qualified name, e.g. 'parentName/suborgName
+ * @return resources which are used by suborganizations of specified organization.
+ * @throws ServerException when any other exception occurs on calculation of used resources
+ */
+ @VisibleForTesting
+ List getUsedResourcesBySuborganizations(String parentQualifiedName)
+ throws NotFoundException, ServerException {
+ ResourceManager resourceManager = resourceManagerProvider.get();
+ List usedResources = new ArrayList<>();
+ for (Organization suborganization :
+ Pages.iterate(
+ (maxItems, skipCount) ->
+ organizationManager.getSuborganizations(
+ parentQualifiedName, maxItems, skipCount))) {
+ usedResources.addAll(resourceManager.getUsedResources(suborganization.getId()));
+ }
+ return usedResources;
+ }
+
+ /** Returns formatted string for list of resources. */
+ private static String format(Collection extends Resource> resources) {
+ return '['
+ + resources.stream()
+ .map(
+ resource -> resource.getAmount() + resource.getUnit() + " of " + resource.getType())
+ .collect(Collectors.joining(", "))
+ + ']';
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/SuborganizationResourcesProvider.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/SuborganizationResourcesProvider.java
new file mode 100644
index 00000000000..6ce55870d2b
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/api/resource/SuborganizationResourcesProvider.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.resource;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toMap;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+import org.eclipse.che.account.api.AccountManager;
+import org.eclipse.che.account.shared.model.Account;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+import org.eclipse.che.multiuser.resource.api.ResourcesProvider;
+import org.eclipse.che.multiuser.resource.api.usage.ResourceManager;
+import org.eclipse.che.multiuser.resource.model.ProvidedResources;
+import org.eclipse.che.multiuser.resource.model.Resource;
+import org.eclipse.che.multiuser.resource.spi.impl.ProvidedResourcesImpl;
+import org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl;
+
+/**
+ * Provides resources that are shared for suborganization by its parent organization.
+ *
+ *
By default suborganizations are able to use parent's resources. Parent organization can limit
+ * usage of resources by suborganization by setting resources caps.
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class SuborganizationResourcesProvider implements ResourcesProvider {
+ public static final String PARENT_RESOURCES_PROVIDER = "parentOrganization";
+
+ private final AccountManager accountManager;
+ private final OrganizationManager organizationManager;
+ private final Provider distributorProvider;
+ private final Provider resourceManagerProvider;
+
+ @Inject
+ public SuborganizationResourcesProvider(
+ AccountManager accountManager,
+ OrganizationManager organizationManager,
+ Provider distributorProvider,
+ Provider resourceManagerProvider) {
+ this.accountManager = accountManager;
+ this.organizationManager = organizationManager;
+ this.distributorProvider = distributorProvider;
+ this.resourceManagerProvider = resourceManagerProvider;
+ }
+
+ @Override
+ public List getResources(String accountId)
+ throws NotFoundException, ServerException {
+ final Account account = accountManager.getById(accountId);
+ String parent;
+
+ if (!OrganizationImpl.ORGANIZATIONAL_ACCOUNT.equals(account.getType())
+ || (parent = organizationManager.getById(accountId).getParent()) == null) {
+ return emptyList();
+ }
+
+ // given account is suborganization's account and can have resources provided by parent
+ List extends Resource> parentTotalResources =
+ resourceManagerProvider.get().getTotalResources(parent);
+
+ if (!parentTotalResources.isEmpty()) {
+ try {
+ List extends Resource> resourcesCaps =
+ distributorProvider.get().getResourcesCaps(accountId);
+
+ return singletonList(
+ new ProvidedResourcesImpl(
+ PARENT_RESOURCES_PROVIDER,
+ null,
+ accountId,
+ -1L,
+ -1L,
+ cap(parentTotalResources, resourcesCaps)));
+ } catch (ConflictException e) {
+ throw new ServerException(e.getLocalizedMessage());
+ }
+ }
+
+ return emptyList();
+ }
+
+ private List cap(
+ Collection extends Resource> source, List extends Resource> caps) {
+ final Map resourcesCaps =
+ caps.stream().collect(toMap(Resource::getType, Function.identity()));
+ return source.stream()
+ .map(
+ resource -> {
+ Resource resourceCap = resourcesCaps.get(resource.getType());
+ if (resourceCap != null) {
+ if (resource.getAmount() == -1) {
+ return resourceCap;
+ } else if (resourceCap.getAmount() < resource.getAmount()) {
+ return resourceCap;
+ }
+ }
+ return resource;
+ })
+ .map(ResourceImpl::new)
+ .collect(Collectors.toList());
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/MemberDao.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/MemberDao.java
new file mode 100644
index 00000000000..279495d4c9a
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/MemberDao.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.spi;
+
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.organization.spi.impl.MemberImpl;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+
+/**
+ * Defines data access object for {@link MemberImpl}
+ *
+ * @author Sergii Leschenko
+ */
+public interface MemberDao {
+ /**
+ * Stores (adds or updates) member.
+ *
+ * @param member member to store
+ * @return optional with updated member, other way empty optional must be returned
+ * @throws NullPointerException when {@code member} is null
+ * @throws ServerException when organization or user doesn't exist
+ * @throws ServerException when any other error occurs during member storing
+ */
+ Optional store(MemberImpl member) throws ServerException;
+
+ /**
+ * Removes member with given organization and user
+ *
+ * @param userId id of user
+ * @param organizationId id of organization
+ * @throws NullPointerException when {@code organizationId} or {@code userId} is null
+ * @throws ServerException when any other error occurs during member removing
+ */
+ void remove(String userId, String organizationId) throws ServerException;
+
+ /**
+ * Returns member for specified organization and user
+ *
+ * @param organizationId organization id
+ * @param userId user id
+ * @return member for specified organization and user
+ * @throws NullPointerException when {@code organizationId} or {@code userId} is null
+ * @throws NotFoundException when member for given user and organization was not found
+ * @throws ServerException when any other error occurs during member fetching
+ */
+ MemberImpl getMember(String organizationId, String userId)
+ throws NotFoundException, ServerException;
+
+ /**
+ * Returns all members of given organization
+ *
+ * @param organizationId organization id
+ * @param maxItems the maximum number of members to return
+ * @param skipCount the number of members to skip
+ * @throws NullPointerException when {@code organizationId} is null
+ * @throws ServerException when any other error occurs during members fetching
+ */
+ Page getMembers(String organizationId, int maxItems, long skipCount)
+ throws ServerException;
+
+ /**
+ * Returns all memberships of given user
+ *
+ * @param userId user id
+ * @throws NullPointerException when {@code userId} is null
+ * @throws ServerException when any other error occurs during members fetching
+ */
+ List getMemberships(String userId) throws ServerException;
+
+ /**
+ * Gets list organizations where user is member.
+ *
+ * @param userId user id
+ * @param maxItems the maximum number of organizations to return
+ * @param skipCount the number of organizations to skip
+ * @return list of organizations where user is member
+ * @throws NullPointerException when {@code userId} is null
+ * @throws ServerException when any other error occurs during organizations fetching
+ */
+ Page getOrganizations(String userId, int maxItems, long skipCount)
+ throws ServerException;
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/OrganizationDao.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/OrganizationDao.java
new file mode 100644
index 00000000000..44352d29b5c
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/OrganizationDao.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.spi;
+
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+
+/**
+ * Defines data access object for {@link OrganizationImpl}
+ *
+ * @author Sergii Leschenko
+ */
+public interface OrganizationDao {
+ /**
+ * Creates organization.
+ *
+ * @param organization organization to create
+ * @throws NullPointerException when {@code organization} is null
+ * @throws ConflictException when organization with such id/name already exists
+ * @throws ServerException when any other error occurs during organization creation
+ */
+ void create(OrganizationImpl organization) throws ServerException, ConflictException;
+
+ /**
+ * Updates organization with new entity.
+ *
+ * @param update organization update
+ * @throws NullPointerException when {@code update} is null
+ * @throws NotFoundException when organization with id {@code organization.getId()} doesn't exist
+ * @throws ConflictException when name updated with a value which is not unique
+ * @throws ServerException when any other error occurs organization updating
+ */
+ void update(OrganizationImpl update) throws NotFoundException, ConflictException, ServerException;
+
+ /**
+ * Removes organization with given id
+ *
+ * @param organizationId organization id
+ * @throws NullPointerException when {@code organizationId} is null
+ * @throws ServerException when any other error occurs during organization removing
+ */
+ void remove(String organizationId) throws ServerException;
+
+ /**
+ * Gets organization by identifier.
+ *
+ * @param organizationId organization id
+ * @return organization instance
+ * @throws NullPointerException when {@code organizationId} is null
+ * @throws NotFoundException when organization with given id was not found
+ * @throws ServerException when any other error occurs during organization fetching
+ */
+ OrganizationImpl getById(String organizationId) throws NotFoundException, ServerException;
+
+ /**
+ * Gets organization by name.
+ *
+ * @param organizationName organization name
+ * @return organization instance
+ * @throws NullPointerException when {@code organizationName} is null
+ * @throws NotFoundException when organization with given name was not found
+ * @throws ServerException when any other error occurs during organization fetching
+ */
+ OrganizationImpl getByName(String organizationName) throws NotFoundException, ServerException;
+
+ /**
+ * Gets child organizations by given parent.
+ *
+ * @param parent id of parent organization
+ * @param maxItems the maximum number of organizations to return
+ * @param skipCount the number of organizations to skip
+ * @return list of children organizations
+ * @throws NullPointerException when {@code parent} is null
+ * @throws ServerException when any other error occurs during organizations fetching
+ */
+ Page getByParent(String parent, int maxItems, long skipCount)
+ throws ServerException;
+
+ /**
+ * Gets all child organizations by specified parent qualified name.
+ *
+ *
Note that the result will includes all direct and nested suborganizations.
+ *
+ * @param parentQualifiedName qualified name of parent organization
+ * @param maxItems the maximum number of organizations to return
+ * @param skipCount the number of organizations to skip
+ * @return list of children organizations
+ * @throws NullPointerException when {@code parentQualifiedName} is null
+ * @throws ServerException when any other error occurs during organizations fetching
+ */
+ Page getSuborganizations(
+ String parentQualifiedName, int maxItems, long skipCount) throws ServerException;
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/OrganizationDistributedResourcesDao.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/OrganizationDistributedResourcesDao.java
new file mode 100644
index 00000000000..be69fd60a06
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/OrganizationDistributedResourcesDao.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.spi;
+
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationDistributedResourcesImpl;
+
+/**
+ * Defines data access object contract for {@link OrganizationDistributedResourcesImpl}.
+ *
+ * @author Sergii Leschenko
+ */
+public interface OrganizationDistributedResourcesDao {
+ /**
+ * Stores (creates or updated) distributed resources for suborganization.
+ *
+ * @param distributedResources distributed resources to store
+ * @throws NullPointerException when either {@code distributedResources} is null
+ * @throws ServerException when any other error occurs
+ */
+ void store(OrganizationDistributedResourcesImpl distributedResources) throws ServerException;
+
+ /**
+ * Returns distributed resources for specified suborganization.
+ *
+ * @param organizationId organization id
+ * @return distributed resources for specified suborganization
+ * @throws NullPointerException when either {@code organizationId} is null
+ * @throws NotFoundException when organization with specified id doesn't have distributed
+ * resources
+ * @throws ServerException when any other error occurs
+ */
+ OrganizationDistributedResourcesImpl get(String organizationId)
+ throws NotFoundException, ServerException;
+
+ /**
+ * Returns distributed resources for suborganizations of given parent organization.
+ *
+ * @param organizationId organization id
+ * @return distributed resources for suborganizations of given parent organization
+ * @throws NullPointerException when either {@code organizationId} is null
+ * @throws ServerException when any other error occurs
+ */
+ Page getByParent(
+ String organizationId, int maxItems, long skipCount) throws ServerException;
+
+ /**
+ * Remove distributed organization resources.
+ *
+ * @param organizationId organization id
+ * @throws NullPointerException when either {@code organizationId} is null
+ * @throws ServerException when any other error occurs
+ */
+ void remove(String organizationId) throws ServerException;
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/impl/MemberImpl.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/impl/MemberImpl.java
new file mode 100644
index 00000000000..c794b14ad69
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/impl/MemberImpl.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.spi.impl;
+
+import java.util.List;
+import javax.persistence.CollectionTable;
+import javax.persistence.Column;
+import javax.persistence.ElementCollection;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+import org.eclipse.che.multiuser.api.permission.server.model.impl.AbstractPermissions;
+import org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain;
+import org.eclipse.che.multiuser.organization.shared.model.Member;
+
+/**
+ * Data object for {@link Member}.
+ *
+ * @author Sergii Leschenko
+ */
+@Entity(name = "Member")
+@NamedQueries({
+ @NamedQuery(
+ name = "Member.getMember",
+ query =
+ "SELECT m "
+ + "FROM Member m "
+ + "WHERE m.userId = :userId AND m.organizationId = :organizationId"),
+ @NamedQuery(
+ name = "Member.getByOrganization",
+ query = "SELECT m " + "FROM Member m " + "WHERE m.organizationId = :organizationId"),
+ @NamedQuery(
+ name = "Member.getCountByOrganizationId",
+ query = "SELECT COUNT(m) " + "FROM Member m " + "WHERE m.organizationId = :organizationId"),
+ @NamedQuery(
+ name = "Member.getByUser",
+ query = "SELECT m " + "FROM Member m " + "WHERE m.userId = :userId"),
+ @NamedQuery(
+ name = "Member.getOrganizations",
+ query = "SELECT org " + "FROM Member m, m.organization org " + "WHERE m.userId = :userId"),
+ @NamedQuery(
+ name = "Member.getOrganizationsCount",
+ query = "SELECT COUNT(m) " + "FROM Member m " + "WHERE m.userId = :userId ")
+})
+@Table(name = "che_member")
+public class MemberImpl extends AbstractPermissions implements Member {
+ @Column(name = "organization_id")
+ private String organizationId;
+
+ @ElementCollection(fetch = FetchType.EAGER)
+ @Column(name = "actions")
+ @CollectionTable(name = "che_member_actions", joinColumns = @JoinColumn(name = "member_id"))
+ protected List actions;
+
+ @ManyToOne
+ @JoinColumn(
+ name = "organization_id",
+ referencedColumnName = "id",
+ insertable = false,
+ updatable = false)
+ private OrganizationImpl organization;
+
+ public MemberImpl() {}
+
+ public MemberImpl(String userId, String organizationId, List actions) {
+ super(userId);
+ this.organizationId = organizationId;
+ if (actions != null) {
+ this.actions = actions;
+ }
+ }
+
+ public MemberImpl(Member member) {
+ this(member.getUserId(), member.getOrganizationId(), member.getActions());
+ }
+
+ @Override
+ public String getInstanceId() {
+ return organizationId;
+ }
+
+ @Override
+ public String getDomainId() {
+ return OrganizationDomain.DOMAIN_ID;
+ }
+
+ @Override
+ public List getActions() {
+ return actions;
+ }
+
+ @Override
+ public String getOrganizationId() {
+ return organizationId;
+ }
+
+ @Override
+ public String toString() {
+ return "MemberImpl{"
+ + "userId='"
+ + userId
+ + '\''
+ + ", organizationId='"
+ + organizationId
+ + '\''
+ + ", actions="
+ + actions
+ + '}';
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/impl/OrganizationDistributedResourcesImpl.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/impl/OrganizationDistributedResourcesImpl.java
new file mode 100644
index 00000000000..8142f144230
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/impl/OrganizationDistributedResourcesImpl.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.spi.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.OneToMany;
+import javax.persistence.PrimaryKeyJoinColumn;
+import javax.persistence.Table;
+import org.eclipse.che.multiuser.organization.shared.model.OrganizationDistributedResources;
+import org.eclipse.che.multiuser.resource.model.Resource;
+import org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl;
+
+/**
+ * Data object for {@link OrganizationDistributedResources}.
+ *
+ * @author Sergii Leschenko
+ */
+@Entity(name = "OrganizationDistributedResources")
+@NamedQueries({
+ @NamedQuery(
+ name = "OrganizationDistributedResources.get",
+ query =
+ "SELECT r "
+ + "FROM OrganizationDistributedResources r "
+ + "WHERE r.organizationId = :organizationId"),
+ @NamedQuery(
+ name = "OrganizationDistributedResources.getByParent",
+ query =
+ "SELECT r "
+ + "FROM OrganizationDistributedResources r "
+ + "WHERE r.organization.parent = :parent"),
+ @NamedQuery(
+ name = "OrganizationDistributedResources.getCountByParent",
+ query =
+ "SELECT COUNT(r) "
+ + "FROM OrganizationDistributedResources r "
+ + "WHERE r.organization.parent = :parent")
+})
+@Table(name = "che_organization_distributed_resources")
+public class OrganizationDistributedResourcesImpl implements OrganizationDistributedResources {
+ @Id
+ @Column(name = "organization_id")
+ private String organizationId;
+
+ @PrimaryKeyJoinColumn private OrganizationImpl organization;
+
+ @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
+ @JoinTable(
+ name = "che_organization_distributed_resources_resource",
+ joinColumns = @JoinColumn(name = "organization_distributed_resources_id"),
+ inverseJoinColumns = @JoinColumn(name = "resource_id"))
+ private List resourcesCap;
+
+ public OrganizationDistributedResourcesImpl() {}
+
+ public OrganizationDistributedResourcesImpl(
+ OrganizationDistributedResources organizationDistributedResource) {
+ this(
+ organizationDistributedResource.getOrganizationId(),
+ organizationDistributedResource.getResourcesCap());
+ }
+
+ public OrganizationDistributedResourcesImpl(
+ String organizationId, List extends Resource> resourcesCap) {
+ this.organizationId = organizationId;
+ if (resourcesCap != null) {
+ this.resourcesCap = resourcesCap.stream().map(ResourceImpl::new).collect(Collectors.toList());
+ }
+ }
+
+ @Override
+ public String getOrganizationId() {
+ return organizationId;
+ }
+
+ @Override
+ public List getResourcesCap() {
+ if (resourcesCap == null) {
+ resourcesCap = new ArrayList<>();
+ }
+ return resourcesCap;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (!(obj instanceof OrganizationDistributedResourcesImpl)) {
+ return false;
+ }
+ final OrganizationDistributedResourcesImpl that = (OrganizationDistributedResourcesImpl) obj;
+ return Objects.equals(organizationId, that.organizationId)
+ && Objects.equals(organization, that.organization)
+ && getResourcesCap().equals(that.getResourcesCap());
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 31 * hash + Objects.hashCode(organizationId);
+ hash = 31 * hash + Objects.hashCode(organization);
+ hash = 31 * hash + getResourcesCap().hashCode();
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ return "OrganizationDistributedResourcesImpl{"
+ + "organizationId='"
+ + organizationId
+ + '\''
+ + ", organization="
+ + organization
+ + ", resourcesCaps="
+ + getResourcesCap()
+ + '}';
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/impl/OrganizationImpl.java b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/impl/OrganizationImpl.java
new file mode 100644
index 00000000000..fa8a56dd02e
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/java/org/eclipse/che/multiuser/organization/spi/impl/OrganizationImpl.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.spi.impl;
+
+import java.util.Objects;
+import javax.persistence.CascadeType;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.ManyToOne;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.OneToOne;
+import javax.persistence.Table;
+import org.eclipse.che.account.spi.AccountImpl;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+
+/**
+ * Data object for {@link Organization}.
+ *
+ * @author Sergii Leschenko
+ */
+@Entity(name = "Organization")
+@NamedQueries({
+ @NamedQuery(
+ name = "Organization.getByName",
+ query = "SELECT o " + "FROM Organization o " + "WHERE o.account.name = :name"),
+ @NamedQuery(
+ name = "Organization.getByParent",
+ query = "SELECT o " + "FROM Organization o " + "WHERE o.parent = :parent "),
+ @NamedQuery(
+ name = "Organization.getByParentCount",
+ query = "SELECT COUNT(o) " + "FROM Organization o " + "WHERE o.parent = :parent "),
+ @NamedQuery(
+ name = "Organization.getSuborganizations",
+ query = "SELECT o " + "FROM Organization o " + "WHERE o.account.name LIKE :qualifiedName "),
+ @NamedQuery(
+ name = "Organization.getSuborganizationsCount",
+ query =
+ "SELECT COUNT(o) " + "FROM Organization o " + "WHERE o.account.name LIKE :qualifiedName ")
+})
+@Table(name = "che_organization")
+public class OrganizationImpl implements Organization {
+ public static final String ORGANIZATIONAL_ACCOUNT = "organizational";
+
+ @Id
+ @Column(name = "id")
+ private String id;
+
+ @OneToOne(cascade = CascadeType.ALL)
+ @JoinColumn(name = "account_id", nullable = false)
+ private AccountImpl account;
+
+ @Column(name = "parent")
+ private String parent;
+
+ // Mapping exists for explicit constraints which allows
+ // jpa backend to perform operations in correct order
+ @ManyToOne
+ @JoinColumn(name = "parent", insertable = false, updatable = false)
+ private OrganizationImpl parentObj;
+
+ public OrganizationImpl() {}
+
+ public OrganizationImpl(Organization organization) {
+ this(organization.getId(), organization.getQualifiedName(), organization.getParent());
+ }
+
+ public OrganizationImpl(String id, String qualifiedName, String parent) {
+ this.id = id;
+ this.account = new AccountImpl(id, qualifiedName, ORGANIZATIONAL_ACCOUNT);
+ this.parent = parent;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ @Override
+ public String getName() {
+ String qualifiedName = getQualifiedName();
+ if (qualifiedName == null) {
+ return null;
+ }
+
+ int lastSlashIndex = qualifiedName.lastIndexOf("/");
+
+ if (lastSlashIndex == -1) {
+ return qualifiedName;
+ }
+
+ return qualifiedName.substring(lastSlashIndex + 1);
+ }
+
+ @Override
+ public String getQualifiedName() {
+ if (account != null) {
+ return account.getName();
+ }
+ return null;
+ }
+
+ public void setQualifiedName(String qualifiedName) {
+ if (account != null) {
+ account.setName(qualifiedName);
+ }
+ }
+
+ @Override
+ public String getParent() {
+ return parent;
+ }
+
+ public void setParent(String parent) {
+ this.parent = parent;
+ }
+
+ public AccountImpl getAccount() {
+ return account;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (!(o instanceof OrganizationImpl)) {
+ return false;
+ }
+ OrganizationImpl that = (OrganizationImpl) o;
+ return Objects.equals(id, that.id)
+ && Objects.equals(getName(), that.getName())
+ && Objects.equals(parent, that.parent);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 31 * hash + Objects.hashCode(id);
+ hash = 31 * hash + Objects.hashCode(getName());
+ hash = 31 * hash + Objects.hashCode(getQualifiedName());
+ hash = 31 * hash + Objects.hashCode(parent);
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ return "OrganizationImpl{"
+ + "id='"
+ + id
+ + '\''
+ + ", name='"
+ + getName()
+ + '\''
+ + ", qualifiedName='"
+ + getQualifiedName()
+ + '\''
+ + ", parent='"
+ + parent
+ + '\''
+ + '}';
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/main/resources/st-html-templates/organization_deleted b/multiuser/api/che-multiuser-api-organization/src/main/resources/st-html-templates/organization_deleted
new file mode 100644
index 00000000000..d19467bc298
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/main/resources/st-html-templates/organization_deleted
@@ -0,0 +1,23 @@
+\
+\
+\
+\
+\
+\
+\
+\
+
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/OrganizationLinksInjectorTest.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/OrganizationLinksInjectorTest.java
new file mode 100644
index 00000000000..4a8f079c933
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/OrganizationLinksInjectorTest.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api;
+
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotNull;
+
+import jakarta.ws.rs.core.UriBuilder;
+import org.eclipse.che.api.core.rest.ServiceContext;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.organization.shared.Constants;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationDto;
+import org.everrest.core.impl.uri.UriBuilderImpl;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link org.eclipse.che.multiuser.organization.api.OrganizationLinksInjector}
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners(MockitoTestNGListener.class)
+public class OrganizationLinksInjectorTest {
+ private static final String URI_BASE = "http://localhost:8080";
+
+ @Mock ServiceContext context;
+
+ OrganizationLinksInjector organizationLinksInjector = new OrganizationLinksInjector();
+
+ @BeforeMethod
+ public void setUp() {
+ final UriBuilder uriBuilder = new UriBuilderImpl();
+ uriBuilder.uri(URI_BASE);
+
+ when(context.getBaseUriBuilder()).thenReturn(uriBuilder);
+ }
+
+ @Test
+ public void shouldInjectLinks() {
+ final OrganizationDto organization = DtoFactory.newDto(OrganizationDto.class).withId("org123");
+
+ final OrganizationDto withLinks = organizationLinksInjector.injectLinks(organization, context);
+
+ assertEquals(withLinks.getLinks().size(), 2);
+ assertNotNull(withLinks.getLink(Constants.LINK_REL_SELF));
+ assertNotNull(withLinks.getLink(Constants.LINK_REL_SUBORGANIZATIONS));
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/OrganizationManagerTest.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/OrganizationManagerTest.java
new file mode 100644
index 00000000000..346cccbc5e1
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/OrganizationManagerTest.java
@@ -0,0 +1,359 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api;
+
+import static java.util.Collections.singletonList;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertNotEquals;
+import static org.testng.Assert.assertNotNull;
+
+import java.util.Collections;
+import java.util.List;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.notification.EventService;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.SubjectImpl;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationDto;
+import org.eclipse.che.multiuser.organization.shared.model.Member;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+import org.eclipse.che.multiuser.organization.spi.MemberDao;
+import org.eclipse.che.multiuser.organization.spi.OrganizationDao;
+import org.eclipse.che.multiuser.organization.spi.impl.MemberImpl;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link org.eclipse.che.multiuser.organization.api.OrganizationManager}
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners(MockitoTestNGListener.class)
+public class OrganizationManagerTest {
+ @Captor private ArgumentCaptor organizationCaptor;
+
+ private static final String USER_NAME = "user-name";
+ private static final String USER_ID = "user-id";
+
+ @Mock private OrganizationDao organizationDao;
+
+ @Mock private MemberDao memberDao;
+
+ @Mock private EventService eventService;
+
+ private OrganizationManager manager;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ manager =
+ spy(
+ new OrganizationManager(
+ eventService, organizationDao, memberDao, new String[] {"reserved"}));
+
+ lenient()
+ .when(eventService.publish(any()))
+ .thenAnswer(invocation -> invocation.getArguments()[0]);
+ EnvironmentContext.getCurrent()
+ .setSubject(new SubjectImpl(USER_NAME, USER_ID, "userToken", false));
+ }
+
+ @AfterMethod
+ public void tearDown() throws Exception {
+ EnvironmentContext.reset();
+ }
+
+ @Test
+ public void shouldCreateOrganization() throws Exception {
+ final Organization toCreate = DtoFactory.newDto(OrganizationDto.class).withName("newOrg");
+
+ manager.create(toCreate);
+
+ verify(organizationDao).create(organizationCaptor.capture());
+ final OrganizationImpl createdOrganization = organizationCaptor.getValue();
+ assertEquals(createdOrganization.getName(), toCreate.getName());
+ assertEquals(createdOrganization.getQualifiedName(), toCreate.getName());
+ assertEquals(createdOrganization.getParent(), toCreate.getParent());
+ verify(memberDao)
+ .store(
+ new MemberImpl(USER_ID, createdOrganization.getId(), OrganizationDomain.getActions()));
+ }
+
+ @Test
+ public void shouldCreateSuborganization() throws Exception {
+ final OrganizationImpl parentOrganization = new OrganizationImpl("org123", "parentOrg", null);
+ when(organizationDao.getById(anyString())).thenReturn(parentOrganization);
+ final Organization toCreate = new OrganizationImpl(null, "orgName", parentOrganization.getId());
+
+ manager.create(toCreate);
+
+ verify(organizationDao).create(organizationCaptor.capture());
+ final OrganizationImpl createdOrganization = organizationCaptor.getValue();
+ assertEquals(createdOrganization.getName(), toCreate.getName());
+ assertEquals(
+ createdOrganization.getQualifiedName(),
+ parentOrganization.getQualifiedName() + "/" + toCreate.getName());
+ assertEquals(createdOrganization.getParent(), toCreate.getParent());
+ verify(memberDao)
+ .store(
+ new MemberImpl(USER_ID, createdOrganization.getId(), OrganizationDomain.getActions()));
+ }
+
+ @Test
+ public void shouldGenerateIdentifierWhenCreatingOrganization() throws Exception {
+ final Organization organization =
+ DtoFactory.newDto(OrganizationDto.class).withName("newOrg").withId("identifier");
+
+ manager.create(organization);
+
+ verify(organizationDao).create(organizationCaptor.capture());
+ final String id = organizationCaptor.getValue().getId();
+ assertNotNull(id);
+ assertNotEquals(id, "identifier");
+ }
+
+ @Test(expectedExceptions = ConflictException.class)
+ public void shouldThrowConflictExceptionOnCreationIfOrganizationNameIsReserved()
+ throws Exception {
+ final Organization organization =
+ DtoFactory.newDto(OrganizationDto.class).withName("reserved").withParent(null);
+
+ manager.create(organization);
+ }
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void shouldThrowNpeWhenCreatingNullableOrganization() throws Exception {
+ manager.create(null);
+ }
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void shouldThrowNpeWhenUpdatingOrganizationWithNullEntity() throws Exception {
+ manager.update("organizationId", null);
+ }
+
+ @Test
+ public void shouldUpdateOrganizationAndIgnoreNewIdAndParentFields() throws Exception {
+ final OrganizationImpl existing = new OrganizationImpl("org123", "oldName", "parent123");
+ final OrganizationImpl expectedExistingToUpdate = new OrganizationImpl(existing);
+ expectedExistingToUpdate.setQualifiedName("newName");
+
+ final OrganizationImpl suborganization =
+ new OrganizationImpl("org321", "oldName/suborgName", "org123");
+ final OrganizationImpl expectedSuborganizationToUpdate = new OrganizationImpl(suborganization);
+ expectedSuborganizationToUpdate.setQualifiedName(
+ expectedExistingToUpdate.getQualifiedName() + "/" + suborganization.getName());
+
+ when(organizationDao.getById(any())).thenReturn(existing);
+ doReturn(new Page<>(singletonList(suborganization), 0, 1, 1))
+ .when(organizationDao)
+ .getSuborganizations(anyString(), anyInt(), anyLong());
+ final OrganizationImpl update = new OrganizationImpl("newId", "newName", "newParentId");
+
+ final Organization updated = manager.update("organizationId", update);
+
+ verify(organizationDao).getById("organizationId");
+ verify(organizationDao, times(2)).update(organizationCaptor.capture());
+ List updatedOrganizations = organizationCaptor.getAllValues();
+ assertEquals(updatedOrganizations.get(0), expectedExistingToUpdate);
+ assertEquals(updatedOrganizations.get(1), expectedSuborganizationToUpdate);
+ verify(organizationDao).getSuborganizations(eq("oldName"), anyInt(), anyLong());
+ assertEquals(updated, expectedExistingToUpdate);
+ }
+
+ @Test(expectedExceptions = ConflictException.class)
+ public void shouldThrowConflictExceptionOnUpdatingIfOrganizationNameIsReserved()
+ throws Exception {
+ when(organizationDao.getById("id")).thenReturn(new OrganizationImpl("id", "oldName", null));
+
+ manager.update("id", new OrganizationImpl("id", "reserved", null));
+ }
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void shouldThrowNpeWhenUpdatingOrganizationByNullId() throws Exception {
+ manager.update(null, new OrganizationImpl());
+ }
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void shouldThrowNpeWhenRemovingOrganizationByNullId() throws Exception {
+ manager.remove(null);
+ }
+
+ @Test
+ public void shouldRemoveOrganization() throws Exception {
+ doNothing().when(manager).removeSuborganizations(anyString());
+ final List members = Collections.singletonList(mock(Member.class));
+ doReturn(members).when(manager).removeMembers(anyString());
+ OrganizationImpl toRemove = new OrganizationImpl("org123", "toRemove", null);
+ when(organizationDao.getById(anyString())).thenReturn(toRemove);
+
+ manager.remove(toRemove.getId());
+
+ verify(organizationDao).remove(toRemove.getId());
+ verify(manager).removeMembers(eq(toRemove.getId()));
+ verify(manager).removeSuborganizations(eq(toRemove.getId()));
+ }
+
+ @Test
+ public void shouldRemoveMembersByOrganizationId() throws Exception {
+ MemberImpl member1 = new MemberImpl("user1", "org1", singletonList("read"));
+ MemberImpl member2 = new MemberImpl("user2", "org1", singletonList("read"));
+ doReturn(new Page<>(singletonList(member1), 0, 1, 2))
+ .doReturn(new Page<>(singletonList(member2), 1, 1, 2))
+ .when(memberDao)
+ .getMembers(anyString(), anyInt(), anyLong());
+
+ manager.removeMembers("org1");
+
+ verify(memberDao, times(2)).getMembers("org1", 100, 0);
+ verify(memberDao).remove("user1", "org1");
+ verify(memberDao).remove("user2", "org1");
+ }
+
+ @Test
+ public void shouldRemoveSuborganizationsByParentOrganizationId() throws Exception {
+ doNothing().when(manager).remove(any());
+ OrganizationImpl subOrg1 = new OrganizationImpl("subOrg1", "subOrg1", "org1");
+ OrganizationImpl subOrg2 = new OrganizationImpl("subOrg2", "subOrg2", "org1");
+ doReturn(new Page<>(singletonList(subOrg1), 0, 1, 2))
+ .doReturn(new Page<>(singletonList(subOrg2), 1, 1, 2))
+ .when(organizationDao)
+ .getByParent(anyString(), anyInt(), anyLong());
+
+ manager.removeSuborganizations("org1");
+
+ verify(organizationDao, times(2)).getByParent("org1", 100, 0);
+ verify(manager).remove("subOrg1");
+ verify(manager).remove("subOrg2");
+ }
+
+ @Test
+ public void shouldNotTryToRemoveOrganizationWhenItIsNotExistRemoveOrganization()
+ throws Exception {
+ when(organizationDao.getById(anyString())).thenThrow(new NotFoundException("not found"));
+
+ manager.remove("id");
+
+ verify(organizationDao, never()).remove(anyString());
+ verify(eventService, never()).publish(any());
+ }
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void shouldThrowNpeWhenGettingOrganizationByNullName() throws Exception {
+ manager.getById(null);
+ }
+
+ @Test
+ public void shouldGetOrganizationByName() throws Exception {
+ final OrganizationImpl toFetch = new OrganizationImpl("org123", "toFetchOrg", "org321");
+ when(organizationDao.getByName(eq("org123"))).thenReturn(toFetch);
+
+ final Organization fetched = manager.getByName("org123");
+
+ assertEquals(fetched, toFetch);
+ verify(organizationDao).getByName("org123");
+ }
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void shouldThrowNpeWhenGettingOrganizationByNullId() throws Exception {
+ manager.getById(null);
+ }
+
+ @Test
+ public void shouldGetOrganizationById() throws Exception {
+ final OrganizationImpl toFetch = new OrganizationImpl("org123", "toFetchOrg", "org321");
+ when(organizationDao.getById(eq("org123"))).thenReturn(toFetch);
+
+ final Organization fetched = manager.getById("org123");
+
+ assertEquals(fetched, toFetch);
+ verify(organizationDao).getById("org123");
+ }
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void shouldThrowNpeWhenGettingSuborganizationsByNullParent() throws Exception {
+ manager.getByParent(null, 30, 0);
+ }
+
+ @Test
+ public void shouldGetOrganizationsByParent() throws Exception {
+ final OrganizationImpl toFetch = new OrganizationImpl("org321", "toFetchOrg", "org123");
+ when(organizationDao.getByParent(eq("org123"), anyInt(), anyLong()))
+ .thenReturn(new Page<>(singletonList(toFetch), 0, 1, 1));
+
+ final Page extends Organization> organizations = manager.getByParent("org123", 30, 0);
+
+ assertEquals(organizations.getItemsCount(), 1);
+ assertEquals(organizations.getItems().get(0), toFetch);
+ verify(organizationDao).getByParent("org123", 30, 0);
+ }
+
+ @Test
+ public void shouldGetSuborganizations() throws Exception {
+ final OrganizationImpl toFetch = new OrganizationImpl("org321", "parent/toFetchOrg", "org123");
+ when(organizationDao.getSuborganizations(eq("parent"), anyInt(), anyLong()))
+ .thenReturn(new Page<>(singletonList(toFetch), 0, 1, 1));
+
+ final Page extends Organization> organizations = manager.getSuborganizations("parent", 30, 0);
+
+ assertEquals(organizations.getItemsCount(), 1);
+ assertEquals(organizations.getItems().get(0), toFetch);
+ verify(organizationDao).getSuborganizations("parent", 30, 0);
+ }
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void shouldThrowNpeOnGettingSuborganizationsByNullParentQualifiedName() throws Exception {
+ manager.getSuborganizations(null, 30, 0);
+ }
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void shouldThrowNpeWhenGettingOrganizationsByNullUserId() throws Exception {
+ manager.getByMember(null, 30, 0);
+ }
+
+ @Test
+ public void shouldGetOrganizationsByMember() throws Exception {
+ final OrganizationImpl toFetch = new OrganizationImpl("org123", "toFetchOrg", "org321");
+ when(memberDao.getOrganizations(eq("org123"), anyInt(), anyLong()))
+ .thenReturn(new Page<>(singletonList(toFetch), 0, 1, 1));
+
+ final Page extends Organization> organizations = manager.getByMember("org123", 30, 0);
+
+ assertEquals(organizations.getItemsCount(), 1);
+ assertEquals(organizations.getItems().get(0), toFetch);
+ verify(memberDao).getOrganizations("org123", 30, 0);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/OrganizationServiceTest.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/OrganizationServiceTest.java
new file mode 100644
index 00000000000..b877a634913
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/OrganizationServiceTest.java
@@ -0,0 +1,345 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api;
+
+import static io.restassured.RestAssured.given;
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toList;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD;
+import static org.everrest.assured.JettyHttpServer.SECURE_PATH;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+import io.restassured.response.Response;
+import java.util.HashSet;
+import java.util.List;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.rest.ApiExceptionMapper;
+import org.eclipse.che.api.core.rest.CheJsonProvider;
+import org.eclipse.che.api.core.rest.shared.dto.ServiceError;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.SubjectImpl;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationDto;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+import org.everrest.assured.EverrestJetty;
+import org.everrest.core.Filter;
+import org.everrest.core.GenericContainerRequest;
+import org.everrest.core.RequestFilter;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link org.eclipse.che.multiuser.organization.api.OrganizationService}.
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners({EverrestJetty.class, MockitoTestNGListener.class})
+public class OrganizationServiceTest {
+
+ private static final String CURRENT_USER_ID = "user123";
+
+ @SuppressWarnings("unused") // is declared for deploying by everrest-assured
+ private ApiExceptionMapper mapper;
+
+ @SuppressWarnings("unused") // is declared for deploying by everrest-assured
+ private EnvironmentFilter filter;
+
+ @SuppressWarnings("unused") // is declared for deploying by everrest-assured
+ private CheJsonProvider jsonProvider = new CheJsonProvider(new HashSet<>());
+
+ @Mock private OrganizationManager orgManager;
+
+ @Mock private OrganizationLinksInjector linksInjector;
+
+ @Mock private OrganizationValidator validator;
+
+ @InjectMocks private OrganizationService service;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ lenient()
+ .when(linksInjector.injectLinks(any(), any()))
+ .thenAnswer(invocation -> invocation.getArguments()[0]);
+ }
+
+ @Test
+ public void shouldCreateOrganization() throws Exception {
+ when(orgManager.create(any()))
+ .thenAnswer(
+ invocationOnMock ->
+ new OrganizationImpl((Organization) invocationOnMock.getArguments()[0]));
+
+ final OrganizationDto toCreate = createOrganization();
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .body(toCreate)
+ .when()
+ .post(SECURE_PATH + "/organization");
+ assertEquals(response.statusCode(), 201);
+ final OrganizationDto createdOrganization = unwrapDto(response, OrganizationDto.class);
+ assertEquals(createdOrganization, toCreate);
+ verify(linksInjector).injectLinks(any(), any());
+ verify(orgManager).create(eq(toCreate));
+ }
+
+ @Test
+ public void shouldThrowBadRequestWhenCreatingNonValidOrganization() throws Exception {
+ doThrow(new BadRequestException("non valid organization"))
+ .when(validator)
+ .checkOrganization(any());
+
+ final OrganizationDto toCreate = createOrganization();
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .body(toCreate)
+ .when()
+ .post(SECURE_PATH + "/organization");
+ assertEquals(response.statusCode(), 400);
+ final ServiceError error = unwrapDto(response, ServiceError.class);
+ assertEquals(error.getMessage(), "non valid organization");
+ verify(validator).checkOrganization(toCreate);
+ }
+
+ @Test
+ public void shouldUpdateOrganization() throws Exception {
+ when(orgManager.update(anyString(), any()))
+ .thenAnswer(
+ invocationOnMock ->
+ new OrganizationImpl((Organization) invocationOnMock.getArguments()[1]));
+
+ final OrganizationDto toUpdate = createOrganization();
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .body(toUpdate)
+ .when()
+ .post(SECURE_PATH + "/organization/organization123");
+ assertEquals(response.statusCode(), 200);
+ final OrganizationDto createdOrganization = unwrapDto(response, OrganizationDto.class);
+ assertEquals(createdOrganization, toUpdate);
+ verify(linksInjector).injectLinks(any(), any());
+ verify(orgManager).update(eq("organization123"), eq(toUpdate));
+ }
+
+ @Test
+ public void shouldThrowBadRequestWhenUpdatingNonValidOrganization() throws Exception {
+ doThrow(new BadRequestException("non valid organization"))
+ .when(validator)
+ .checkOrganization(any());
+
+ final OrganizationDto toUpdate = createOrganization();
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .body(toUpdate)
+ .when()
+ .post(SECURE_PATH + "/organization/organization123");
+ assertEquals(response.statusCode(), 400);
+ final ServiceError error = unwrapDto(response, ServiceError.class);
+ assertEquals(error.getMessage(), "non valid organization");
+ verify(validator).checkOrganization(toUpdate);
+ }
+
+ @Test
+ public void shouldRemoveOrganization() throws Exception {
+ Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .delete(SECURE_PATH + "/organization/organization123");
+ assertEquals(response.statusCode(), 204);
+ verify(orgManager).remove(eq("organization123"));
+ }
+
+ @Test
+ public void shouldGetOrganizationById() throws Exception {
+ final OrganizationDto toFetch = createOrganization();
+
+ when(orgManager.getById(eq("organization123"))).thenReturn(toFetch);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .get(SECURE_PATH + "/organization/organization123");
+
+ assertEquals(response.statusCode(), 200);
+ final OrganizationDto fetchedOrganization = unwrapDto(response, OrganizationDto.class);
+ assertEquals(fetchedOrganization, toFetch);
+ verify(orgManager).getById(eq("organization123"));
+ verify(linksInjector).injectLinks(any(), any());
+ }
+
+ @Test
+ public void shouldFindOrganizationByName() throws Exception {
+ final OrganizationDto toFetch = createOrganization();
+
+ when(orgManager.getByName(eq("subOrg"))).thenReturn(toFetch);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .get(SECURE_PATH + "/organization/find?name=subOrg");
+ assertEquals(response.statusCode(), 200);
+ final OrganizationDto fetchedOrganization = unwrapDto(response, OrganizationDto.class);
+ assertEquals(fetchedOrganization, toFetch);
+ verify(orgManager).getByName(eq("subOrg"));
+ verify(linksInjector).injectLinks(any(), any());
+ }
+
+ @Test
+ public void shouldThrowBadRequestExceptionWhenFindingOrganizationWithoutName() throws Exception {
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .get(SECURE_PATH + "/organization/find")
+ .then()
+ .assertThat()
+ .statusCode(400);
+ }
+
+ @Test
+ public void shouldGetChildOrganizations() throws Exception {
+ final OrganizationDto toFetch = createOrganization();
+
+ doReturn(new Page<>(singletonList(toFetch), 0, 1, 1))
+ .when(orgManager)
+ .getByParent(anyString(), anyInt(), anyLong());
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .get(SECURE_PATH + "/organization/parentOrg123/organizations?skipCount=0&maxItems=1");
+ assertEquals(response.statusCode(), 200);
+ final List organizationDtos = unwrapDtoList(response, OrganizationDto.class);
+ assertEquals(organizationDtos.size(), 1);
+ assertEquals(organizationDtos.get(0), toFetch);
+ verify(orgManager).getByParent("parentOrg123", 1, 0);
+ verify(linksInjector).injectLinks(any(), any());
+ }
+
+ @Test
+ public void shouldGetOrganizationsByCurrentUserIfParameterIsNotSpecified() throws Exception {
+ final OrganizationDto toFetch = createOrganization();
+
+ doReturn(new Page<>(singletonList(toFetch), 0, 1, 1))
+ .when(orgManager)
+ .getByMember(anyString(), anyInt(), anyInt());
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .get(SECURE_PATH + "/organization?skipCount=0&maxItems=1");
+ assertEquals(response.statusCode(), 200);
+ final List organizationDtos = unwrapDtoList(response, OrganizationDto.class);
+ assertEquals(organizationDtos.size(), 1);
+ assertEquals(organizationDtos.get(0), toFetch);
+ verify(orgManager).getByMember(CURRENT_USER_ID, 1, 0);
+ verify(linksInjector).injectLinks(any(), any());
+ }
+
+ @Test
+ public void shouldGetOrganizationsBySpecifiedUser() throws Exception {
+ final OrganizationDto toFetch = createOrganization();
+
+ doReturn(new Page<>(singletonList(toFetch), 0, 1, 1))
+ .when(orgManager)
+ .getByMember(anyString(), anyInt(), anyInt());
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .get(SECURE_PATH + "/organization?user=user789&skipCount=0&maxItems=1");
+ assertEquals(response.statusCode(), 200);
+ final List organizationDtos = unwrapDtoList(response, OrganizationDto.class);
+ assertEquals(organizationDtos.size(), 1);
+ assertEquals(organizationDtos.get(0), toFetch);
+ verify(orgManager).getByMember("user789", 1, 0);
+ verify(linksInjector).injectLinks(any(), any());
+ }
+
+ private static T unwrapDto(Response response, Class dtoClass) {
+ return DtoFactory.getInstance().createDtoFromJson(response.body().print(), dtoClass);
+ }
+
+ private static List unwrapDtoList(Response response, Class dtoClass) {
+ return DtoFactory.getInstance()
+ .createListDtoFromJson(response.body().print(), dtoClass)
+ .stream()
+ .collect(toList());
+ }
+
+ private OrganizationDto createOrganization() {
+ return DtoFactory.newDto(OrganizationDto.class)
+ .withId("organization123")
+ .withName("subOrg")
+ .withQualifiedName("parentOrg/subOrg")
+ .withParent("parentOrg123");
+ }
+
+ @Filter
+ public static class EnvironmentFilter implements RequestFilter {
+ @Override
+ public void doFilter(GenericContainerRequest request) {
+ EnvironmentContext.getCurrent()
+ .setSubject(new SubjectImpl("userName", CURRENT_USER_ID, "token", false));
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationPermissionsFilterTest.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationPermissionsFilterTest.java
new file mode 100644
index 00000000000..8288a748f2a
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationPermissionsFilterTest.java
@@ -0,0 +1,499 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.permissions;
+
+import static io.restassured.RestAssured.given;
+import static org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain.DELETE;
+import static org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain.DOMAIN_ID;
+import static org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain.MANAGE_SUBORGANIZATIONS;
+import static org.eclipse.che.multiuser.organization.api.permissions.OrganizationDomain.UPDATE;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD;
+import static org.everrest.assured.JettyHttpServer.SECURE_PATH;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import io.restassured.response.Response;
+import io.restassured.specification.RequestSpecification;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.rest.ApiExceptionMapper;
+import org.eclipse.che.api.core.rest.shared.dto.ServiceError;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.api.permission.server.SuperPrivilegesChecker;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.api.OrganizationService;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationDto;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+import org.everrest.assured.EverrestJetty;
+import org.everrest.core.Filter;
+import org.everrest.core.GenericContainerRequest;
+import org.everrest.core.RequestFilter;
+import org.everrest.core.resource.GenericResourceMethod;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link
+ * org.eclipse.che.multiuser.organization.api.permissions.OrganizationPermissionsFilter}
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners(value = {EverrestJetty.class, MockitoTestNGListener.class})
+public class OrganizationPermissionsFilterTest {
+ @SuppressWarnings("unused")
+ private static final ApiExceptionMapper MAPPER = new ApiExceptionMapper();
+
+ @SuppressWarnings("unused")
+ private static final EnvironmentFilter FILTER = new EnvironmentFilter();
+
+ private static final String USER_ID = "user123";
+
+ @Mock private static Subject subject;
+
+ @Mock private OrganizationService service;
+
+ @Mock private OrganizationManager manager;
+
+ @Mock private SuperPrivilegesChecker superPrivilegesChecker;
+
+ @InjectMocks private OrganizationPermissionsFilter permissionsFilter;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ lenient().when(subject.getUserId()).thenReturn(USER_ID);
+
+ lenient()
+ .when(manager.getById(anyString()))
+ .thenReturn(new OrganizationImpl("organization123", "test", null));
+ }
+
+ @Test
+ public void shouldTestThatAllPublicMethodsAreCoveredByPermissionsFilter() throws Exception {
+ // given
+ final List collect =
+ Stream.of(OrganizationService.class.getDeclaredMethods())
+ .filter(method -> Modifier.isPublic(method.getModifiers()))
+ .map(Method::getName)
+ .collect(Collectors.toList());
+
+ // then
+ assertEquals(collect.size(), 7);
+ assertTrue(collect.contains(OrganizationPermissionsFilter.CREATE_METHOD));
+ assertTrue(collect.contains(OrganizationPermissionsFilter.UPDATE_METHOD));
+ assertTrue(collect.contains(OrganizationPermissionsFilter.REMOVE_METHOD));
+ assertTrue(collect.contains(OrganizationPermissionsFilter.GET_BY_PARENT_METHOD));
+ assertTrue(collect.contains(OrganizationPermissionsFilter.GET_ORGANIZATIONS_METHOD));
+ assertTrue(collect.contains(OrganizationPermissionsFilter.GET_BY_ID_METHOD));
+ assertTrue(collect.contains(OrganizationPermissionsFilter.FIND_METHOD));
+ }
+
+ @Test
+ public void shouldNotCheckPermissionsOnGettingOrganizationById() throws Exception {
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .get(SECURE_PATH + "/organization/organization123");
+
+ verify(service).getById("organization123");
+ verifyNoMoreInteractions(subject);
+ }
+
+ @Test
+ public void shouldNotCheckPermissionsOnGettingOrganizationByName() throws Exception {
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .get(SECURE_PATH + "/organization/find?name=test");
+
+ verify(service).find("test");
+ verifyNoMoreInteractions(subject);
+ }
+
+ @Test
+ public void shouldNotCheckPermissionsOnOrganizationsFetchingIfUserIdIsNotSpecified()
+ throws Exception {
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .expect()
+ .statusCode(204)
+ .when()
+ .get(SECURE_PATH + "/organization");
+
+ verify(service).getOrganizations(eq(null), anyInt(), anyInt());
+ verify(subject, never()).hasPermission(anyString(), anyString(), anyString());
+ }
+
+ @Test
+ public void shouldNotCheckPermissionsOnOrganizationsFetchingIfUserSpecifiesHisOwnId()
+ throws Exception {
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .expect()
+ .statusCode(204)
+ .when()
+ .get(SECURE_PATH + "/organization?user=" + USER_ID);
+
+ verify(service).getOrganizations(eq(USER_ID), anyInt(), anyInt());
+ verify(subject, never()).hasPermission(anyString(), anyString(), anyString());
+ }
+
+ @Test
+ public void shouldCheckSuperPrivilegesOnOrganizationsFetchingIfUserSpecifiesForeignId()
+ throws Exception {
+ when(superPrivilegesChecker.hasSuperPrivileges()).thenReturn(true);
+
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .expect()
+ .statusCode(204)
+ .when()
+ .get(SECURE_PATH + "/organization?user=user321");
+
+ verify(service).getOrganizations(eq("user321"), anyInt(), anyInt());
+ verify(superPrivilegesChecker).hasSuperPrivileges();
+ }
+
+ @Test
+ public void
+ shouldThrowForbiddenExceptionOnOrganizationsFetchingIfUserSpecifiesForeignIdAndDoesNotHaveSuperPrivileges()
+ throws Exception {
+ when(superPrivilegesChecker.hasSuperPrivileges()).thenReturn(false);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .expect()
+ .statusCode(403)
+ .when()
+ .get(SECURE_PATH + "/organization?user=user321");
+
+ assertEquals(unwrapError(response), "The user is able to specify only his own id");
+ verify(superPrivilegesChecker).hasSuperPrivileges();
+ verifyNoMoreInteractions(service);
+ }
+
+ @Test
+ public void shouldCheckPermissionsOnOrganizationUpdating() throws Exception {
+ when(subject.hasPermission(DOMAIN_ID, "organization123", UPDATE)).thenReturn(true);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .post(SECURE_PATH + "/organization/organization123");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(service).update(eq("organization123"), any());
+ verify(subject).hasPermission(DOMAIN_ID, "organization123", UPDATE);
+ verify(superPrivilegesChecker, never()).hasSuperPrivileges();
+ verifyNoMoreInteractions(subject);
+ }
+
+ @Test
+ public void shouldCheckPermissionsOnParentOrgLevelOnChildOrganizationUpdating() throws Exception {
+ when(manager.getById(anyString()))
+ .thenReturn(new OrganizationImpl("organization123", "test", "parent123"));
+ when(subject.hasPermission(DOMAIN_ID, "parent123", MANAGE_SUBORGANIZATIONS)).thenReturn(true);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .post(SECURE_PATH + "/organization/organization123");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(service).update(eq("organization123"), any());
+ verify(subject).hasPermission(DOMAIN_ID, "parent123", MANAGE_SUBORGANIZATIONS);
+ verify(superPrivilegesChecker, never()).hasSuperPrivileges();
+ verifyNoMoreInteractions(subject);
+ }
+
+ @Test
+ public void
+ shouldCheckPermissionsOnChildOrganizationUpdatingWhenUserDoesNotHavePermissionsOnParentOrgLevel()
+ throws Exception {
+ when(manager.getById(anyString()))
+ .thenReturn(new OrganizationImpl("organization123", "test", "parent123"));
+ doReturn(false).when(subject).hasPermission(DOMAIN_ID, "parent123", MANAGE_SUBORGANIZATIONS);
+ doReturn(true).when(subject).hasPermission(DOMAIN_ID, "organization123", UPDATE);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .post(SECURE_PATH + "/organization/organization123");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(service).update(eq("organization123"), any());
+ verify(subject).hasPermission(DOMAIN_ID, "parent123", MANAGE_SUBORGANIZATIONS);
+ verify(subject).hasPermission(DOMAIN_ID, "organization123", UPDATE);
+ }
+
+ @Test
+ public void shouldCheckPermissionsOnOrganizationRemoving() throws Exception {
+ when(subject.hasPermission(DOMAIN_ID, "organization123", DELETE)).thenReturn(true);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .delete(SECURE_PATH + "/organization/organization123");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(service).remove(eq("organization123"));
+ verify(subject).hasPermission(DOMAIN_ID, "organization123", DELETE);
+ verify(superPrivilegesChecker, never()).hasSuperPrivileges();
+ verifyNoMoreInteractions(subject);
+ }
+
+ @Test
+ public void shouldCheckPermissionsOnParentOrgLevelOnChildOrganizationRemoving() throws Exception {
+ when(manager.getById(anyString()))
+ .thenReturn(new OrganizationImpl("organization123", "test", "parent123"));
+ when(subject.hasPermission(DOMAIN_ID, "parent123", MANAGE_SUBORGANIZATIONS)).thenReturn(true);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .delete(SECURE_PATH + "/organization/organization123");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(service).remove(eq("organization123"));
+ verify(subject).hasPermission(DOMAIN_ID, "parent123", MANAGE_SUBORGANIZATIONS);
+ verify(superPrivilegesChecker, never()).hasSuperPrivileges();
+ verifyNoMoreInteractions(subject);
+ }
+
+ @Test
+ public void
+ shouldCheckPermissionsOnChildOrganizationRemovingWhenUserDoesNotHavePermissionsOnParentOrgLevel()
+ throws Exception {
+ when(manager.getById(anyString()))
+ .thenReturn(new OrganizationImpl("organization123", "test", "parent123"));
+ doReturn(false).when(subject).hasPermission(DOMAIN_ID, "parent123", MANAGE_SUBORGANIZATIONS);
+ doReturn(true).when(subject).hasPermission(DOMAIN_ID, "organization123", DELETE);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .delete(SECURE_PATH + "/organization/organization123");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(service).remove(eq("organization123"));
+ verify(subject).hasPermission(DOMAIN_ID, "parent123", MANAGE_SUBORGANIZATIONS);
+ verify(subject).hasPermission(DOMAIN_ID, "organization123", DELETE);
+ verify(superPrivilegesChecker, never()).hasSuperPrivileges();
+ verifyNoMoreInteractions(subject);
+ }
+
+ @Test
+ public void shouldNotCheckPermissionsOnRootOrganizationCreation() throws Exception {
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .body(DtoFactory.newDto(OrganizationDto.class).withParent(null))
+ .post(SECURE_PATH + "/organization");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(service).create(any());
+ verifyNoMoreInteractions(subject);
+ }
+
+ @Test
+ public void shouldCheckPermissionsOnChildOrganizationCreation() throws Exception {
+ when(subject.hasPermission(DOMAIN_ID, "parent-org", MANAGE_SUBORGANIZATIONS)).thenReturn(true);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .body(DtoFactory.newDto(OrganizationDto.class).withParent("parent-org"))
+ .post(SECURE_PATH + "/organization");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(service).create(any());
+ verify(subject).hasPermission(DOMAIN_ID, "parent-org", MANAGE_SUBORGANIZATIONS);
+ }
+
+ @Test
+ public void
+ shouldThrowForbiddenExceptionOnChildOrganizationCreationIfUserDoesNotHaveCorrespondingPermission()
+ throws Exception {
+ when(subject.hasPermission(DOMAIN_ID, "parent-org", MANAGE_SUBORGANIZATIONS)).thenReturn(false);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .body(DtoFactory.newDto(OrganizationDto.class).withParent("parent-org"))
+ .post(SECURE_PATH + "/organization");
+
+ assertEquals(response.getStatusCode(), 403);
+ verifyNoMoreInteractions(service);
+ verify(subject).hasPermission(DOMAIN_ID, "parent-org", MANAGE_SUBORGANIZATIONS);
+ }
+
+ @Test(
+ expectedExceptions = ForbiddenException.class,
+ expectedExceptionsMessageRegExp =
+ "The user does not have permission to perform this operation")
+ public void shouldThrowForbiddenExceptionWhenRequestedUnknownMethod() throws Exception {
+ final GenericResourceMethod mock = mock(GenericResourceMethod.class);
+ Method injectLinks = OrganizationService.class.getMethod("getServiceDescriptor");
+ when(mock.getMethod()).thenReturn(injectLinks);
+
+ permissionsFilter.filter(mock, new Object[] {});
+ }
+
+ @Test(dataProvider = "coveredPaths")
+ public void shouldThrowForbiddenExceptionWhenUserDoesNotHavePermissionsForPerformOperation(
+ String path, String method, String action) throws Exception {
+ when(subject.hasPermission(anyString(), anyString(), anyString())).thenReturn(false);
+
+ Response response =
+ request(
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when(),
+ SECURE_PATH + path,
+ method);
+
+ assertEquals(response.getStatusCode(), 403);
+ assertEquals(
+ unwrapError(response),
+ "The user does not have permission to "
+ + action
+ + " organization with id 'organization123'");
+
+ verifyNoMoreInteractions(service);
+ }
+
+ @Test(dataProvider = "coveredPaths")
+ public void shouldThrowNotFoundWhenUserRequestsNonExistedOrganization(
+ String path, String method, String ignored) throws Exception {
+ when(manager.getById(anyString()))
+ .thenThrow(new NotFoundException("Organization was not found"));
+
+ Response response =
+ request(
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when(),
+ SECURE_PATH + path,
+ method);
+
+ assertEquals(response.getStatusCode(), 404);
+ assertEquals(unwrapError(response), "Organization was not found");
+
+ verifyNoMoreInteractions(service);
+ }
+
+ @DataProvider(name = "coveredPaths")
+ public Object[][] pathsProvider() {
+ return new Object[][] {
+ {"/organization/organization123", "post", UPDATE},
+ {"/organization/organization123", "delete", DELETE},
+ {"/organization/organization123/organizations", "get", MANAGE_SUBORGANIZATIONS}
+ };
+ }
+
+ private Response request(RequestSpecification request, String path, String method) {
+ switch (method) {
+ case "post":
+ return request.post(path);
+ case "get":
+ return request.get(path);
+ case "delete":
+ return request.delete(path);
+ case "put":
+ return request.put(path);
+ }
+ throw new RuntimeException("Unsupported method");
+ }
+
+ private static String unwrapError(Response response) {
+ return unwrapDto(response, ServiceError.class).getMessage();
+ }
+
+ private static T unwrapDto(Response response, Class dtoClass) {
+ return DtoFactory.getInstance().createDtoFromJson(response.body().print(), dtoClass);
+ }
+
+ @Filter
+ public static class EnvironmentFilter implements RequestFilter {
+ @Override
+ public void doFilter(GenericContainerRequest request) {
+ EnvironmentContext.getCurrent().setSubject(subject);
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationRemoteSubscriptionPermissionsChecksTest.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationRemoteSubscriptionPermissionsChecksTest.java
new file mode 100644
index 00000000000..b8b200c8953
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationRemoteSubscriptionPermissionsChecksTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.permissions;
+
+import static org.eclipse.che.multiuser.organization.api.listener.OrganizationEventsWebsocketBroadcaster.ORGANIZATION_CHANGED_METHOD_NAME;
+import static org.eclipse.che.multiuser.organization.api.listener.OrganizationEventsWebsocketBroadcaster.ORGANIZATION_MEMBERSHIP_METHOD_NAME;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.common.collect.ImmutableMap;
+import java.util.Collections;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.multiuser.api.permission.server.PermissionsManager;
+import org.eclipse.che.multiuser.api.permission.server.jsonrpc.RemoteSubscriptionPermissionManager;
+import org.eclipse.che.multiuser.api.permission.server.model.impl.AbstractPermissions;
+import org.eclipse.che.multiuser.organization.api.permissions.OrganizationRemoteSubscriptionPermissionsChecks.MembershipsChangedSubscriptionCheck;
+import org.eclipse.che.multiuser.organization.api.permissions.OrganizationRemoteSubscriptionPermissionsChecks.OrganizationChangedSubscriptionCheck;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests {@link OrganizationRemoteSubscriptionPermissionsChecks}.
+ *
+ * @author Sergii Leshchenko
+ */
+@Listeners(MockitoTestNGListener.class)
+public class OrganizationRemoteSubscriptionPermissionsChecksTest {
+ @Mock private Subject subject;
+
+ @Mock private PermissionsManager permissionsManager;
+ @Mock private RemoteSubscriptionPermissionManager permissionManager;
+
+ @InjectMocks private OrganizationRemoteSubscriptionPermissionsChecks permissionsChecks;
+
+ @BeforeMethod
+ public void setUp() {
+ EnvironmentContext.getCurrent().setSubject(subject);
+ }
+
+ @AfterMethod
+ public void tearDown() {
+ EnvironmentContext.reset();
+ }
+
+ @Test
+ public void shouldRegisterChecks() {
+ // when
+ permissionsChecks.register(permissionManager);
+
+ // then
+ verify(permissionManager)
+ .registerCheck(
+ any(OrganizationChangedSubscriptionCheck.class), eq(ORGANIZATION_CHANGED_METHOD_NAME));
+ verify(permissionManager)
+ .registerCheck(
+ any(MembershipsChangedSubscriptionCheck.class),
+ eq(ORGANIZATION_MEMBERSHIP_METHOD_NAME));
+ }
+
+ @Test(
+ expectedExceptions = ForbiddenException.class,
+ expectedExceptionsMessageRegExp = "User id must be specified in scope")
+ public void shouldThrowExceptionIfUserIdIsMissing() throws Exception {
+ // given
+ MembershipsChangedSubscriptionCheck check = new MembershipsChangedSubscriptionCheck();
+ when(subject.getUserId()).thenReturn("user2");
+
+ // when
+ check.check(ORGANIZATION_MEMBERSHIP_METHOD_NAME, Collections.emptyMap());
+ }
+
+ @Test(
+ expectedExceptions = ForbiddenException.class,
+ expectedExceptionsMessageRegExp = "It is only allowed to listen to own memberships changes")
+ public void shouldThrowExceptionIfUserTryToListenToForeignMemberships() throws Exception {
+ // given
+ MembershipsChangedSubscriptionCheck check = new MembershipsChangedSubscriptionCheck();
+ when(subject.getUserId()).thenReturn("user2");
+
+ // when
+ check.check(ORGANIZATION_MEMBERSHIP_METHOD_NAME, ImmutableMap.of("userId", "user1"));
+ }
+
+ @Test
+ public void shouldDoNothingIfUserTryToListenToOwnMemberships() throws Exception {
+ // given
+ MembershipsChangedSubscriptionCheck check = new MembershipsChangedSubscriptionCheck();
+ when(subject.getUserId()).thenReturn("user1");
+
+ // when
+ check.check(ORGANIZATION_MEMBERSHIP_METHOD_NAME, ImmutableMap.of("userId", "user1"));
+ }
+
+ @Test(
+ expectedExceptions = ForbiddenException.class,
+ expectedExceptionsMessageRegExp = "Organization id must be specified in scope")
+ public void shouldThrowExceptionIfOrganizationIdIsMissing() throws Exception {
+ // given
+ OrganizationChangedSubscriptionCheck check =
+ new OrganizationChangedSubscriptionCheck(permissionsManager);
+
+ // when
+ check.check(ORGANIZATION_MEMBERSHIP_METHOD_NAME, Collections.emptyMap());
+ }
+
+ @Test(
+ expectedExceptions = ForbiddenException.class,
+ expectedExceptionsMessageRegExp =
+ "User doesn't have any permissions for the specified organization")
+ public void shouldThrowExceptionIfUserDoesNotHaveAnyPermissionsToRequestedOrganization()
+ throws Exception {
+ // given
+ OrganizationChangedSubscriptionCheck check =
+ new OrganizationChangedSubscriptionCheck(permissionsManager);
+ when(subject.getUserId()).thenReturn("user1");
+ when(permissionsManager.get("user1", OrganizationDomain.DOMAIN_ID, "org123"))
+ .thenThrow(new NotFoundException(""));
+
+ // when
+ check.check(ORGANIZATION_MEMBERSHIP_METHOD_NAME, ImmutableMap.of("organizationId", "org123"));
+ }
+
+ @Test
+ public void shouldDoNothingIfUserTryToListenEventsOfOrganizationWhereHeHasPermissions()
+ throws Exception {
+ // given
+ OrganizationChangedSubscriptionCheck check =
+ new OrganizationChangedSubscriptionCheck(permissionsManager);
+ when(subject.getUserId()).thenReturn("user1");
+ when(permissionsManager.get("user1", OrganizationDomain.DOMAIN_ID, "org123"))
+ .thenReturn(mock(AbstractPermissions.class));
+
+ // when
+ check.check(ORGANIZATION_MEMBERSHIP_METHOD_NAME, ImmutableMap.of("organizationId", "org123"));
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationResourceDistributionServicePermissionsFilterTest.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationResourceDistributionServicePermissionsFilterTest.java
new file mode 100644
index 00000000000..d774a58c731
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationResourceDistributionServicePermissionsFilterTest.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.permissions;
+
+import static io.restassured.RestAssured.given;
+import static java.util.Collections.emptyList;
+import static java.util.stream.Collectors.toList;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD;
+import static org.everrest.assured.JettyHttpServer.SECURE_PATH;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import jakarta.ws.rs.core.MediaType;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Stream;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.rest.ApiExceptionMapper;
+import org.eclipse.che.api.core.rest.CheJsonProvider;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.api.permission.server.SuperPrivilegesChecker;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.api.resource.OrganizationResourcesDistributionService;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+import org.eclipse.che.multiuser.resource.shared.dto.ResourceDto;
+import org.everrest.assured.EverrestJetty;
+import org.everrest.core.Filter;
+import org.everrest.core.GenericContainerRequest;
+import org.everrest.core.RequestFilter;
+import org.everrest.core.resource.GenericResourceMethod;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link
+ * org.eclipse.che.multiuser.organization.api.permissions.OrganizationResourceDistributionServicePermissionsFilter}
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners({EverrestJetty.class, MockitoTestNGListener.class})
+public class OrganizationResourceDistributionServicePermissionsFilterTest {
+ @SuppressWarnings("unused")
+ private static final ApiExceptionMapper MAPPER = new ApiExceptionMapper();
+
+ @SuppressWarnings("unused")
+ private static final EnvironmentFilter FILTER = new EnvironmentFilter();
+
+ @SuppressWarnings("unused")
+ private static final CheJsonProvider JSON_PROVIDER = new CheJsonProvider(new HashSet<>());
+
+ private static final String SUBORGANIZATION = "org123";
+ private static final String PARENT_ORGANIZATION = "parentOrg123";
+
+ @Mock private static Subject subject;
+
+ @Mock private OrganizationResourcesDistributionService service;
+
+ @Mock private OrganizationManager manager;
+
+ @Mock private SuperPrivilegesChecker superPrivilegesChecker;
+
+ @InjectMocks private OrganizationResourceDistributionServicePermissionsFilter permissionsFilter;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ lenient()
+ .when(manager.getById(SUBORGANIZATION))
+ .thenReturn(new OrganizationImpl(SUBORGANIZATION, "testOrg", PARENT_ORGANIZATION));
+ lenient()
+ .when(manager.getById(PARENT_ORGANIZATION))
+ .thenReturn(new OrganizationImpl(PARENT_ORGANIZATION, "parentOrg", null));
+
+ lenient().when(subject.hasPermission(anyString(), anyString(), anyString())).thenReturn(true);
+ }
+
+ @Test
+ public void shouldTestThatAllPublicMethodsAreCoveredByPermissionsFilter() throws Exception {
+ // given
+ final List collect =
+ Stream.of(OrganizationResourcesDistributionService.class.getDeclaredMethods())
+ .filter(method -> Modifier.isPublic(method.getModifiers()))
+ .map(Method::getName)
+ .collect(toList());
+
+ // then
+ assertEquals(collect.size(), 3);
+ assertTrue(
+ collect.contains(
+ OrganizationResourceDistributionServicePermissionsFilter.CAP_RESOURCES_METHOD));
+ assertTrue(
+ collect.contains(
+ OrganizationResourceDistributionServicePermissionsFilter.GET_RESOURCES_CAP_METHOD));
+ assertTrue(
+ collect.contains(
+ OrganizationResourceDistributionServicePermissionsFilter.GET_DISTRIBUTED_RESOURCES));
+ }
+
+ @Test
+ public void shouldCheckManageResourcesPermissionsOnResourcesCappingForSuborganization()
+ throws Exception {
+ List resources =
+ Collections.singletonList(
+ DtoFactory.newDto(ResourceDto.class).withType("test").withAmount(123).withUnit("unit"));
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType(MediaType.APPLICATION_JSON)
+ .body(resources)
+ .expect()
+ .statusCode(204)
+ .when()
+ .post(SECURE_PATH + "/organization/resource/" + SUBORGANIZATION + "/cap");
+
+ verify(service).capResources(SUBORGANIZATION, resources);
+ verify(subject)
+ .hasPermission(
+ OrganizationDomain.DOMAIN_ID, PARENT_ORGANIZATION, OrganizationDomain.MANAGE_RESOURCES);
+ }
+
+ @Test
+ public void shouldNotCheckPermissionsOnResourcesCappingForRootOrganization() throws Exception {
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType(MediaType.APPLICATION_JSON)
+ .body(emptyList())
+ .expect()
+ .statusCode(204)
+ .when()
+ .post(SECURE_PATH + "/organization/resource/" + PARENT_ORGANIZATION + "/cap");
+
+ verify(service).capResources(PARENT_ORGANIZATION, emptyList());
+ verify(subject, never()).hasPermission(anyString(), anyString(), anyString());
+ }
+
+ @Test
+ public void
+ shouldCheckManageResourcesPermissionsOnGettingDistributedResourcesWhenUserDoesNotHaveSuperPrivileges()
+ throws Exception {
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .expect()
+ .statusCode(204)
+ .when()
+ .get(SECURE_PATH + "/organization/resource/" + PARENT_ORGANIZATION);
+
+ verify(service).getDistributedResources(eq(PARENT_ORGANIZATION), anyInt(), anyLong());
+ verify(subject)
+ .hasPermission(
+ OrganizationDomain.DOMAIN_ID, PARENT_ORGANIZATION, OrganizationDomain.MANAGE_RESOURCES);
+ verify(superPrivilegesChecker).hasSuperPrivileges();
+ }
+
+ @Test
+ public void
+ shouldNotCheckManageResourcesPermissionsOnGettingDistributedResourcesWhenUserHasSuperPrivileges()
+ throws Exception {
+ when(superPrivilegesChecker.hasSuperPrivileges()).thenReturn(true);
+
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .expect()
+ .statusCode(204)
+ .when()
+ .get(SECURE_PATH + "/organization/resource/" + PARENT_ORGANIZATION);
+
+ verify(service).getDistributedResources(eq(PARENT_ORGANIZATION), anyInt(), anyLong());
+ verify(subject, never())
+ .hasPermission(
+ OrganizationDomain.DOMAIN_ID, PARENT_ORGANIZATION, OrganizationDomain.MANAGE_RESOURCES);
+ verify(superPrivilegesChecker).hasSuperPrivileges();
+ }
+
+ @Test
+ public void
+ shouldCheckManageResourcesPermissionsOnGettingResourcesCapWhenUserDoesNotHaveSuperPrivileges()
+ throws Exception {
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .expect()
+ .statusCode(200)
+ .when()
+ .get(SECURE_PATH + "/organization/resource/" + SUBORGANIZATION + "/cap");
+
+ verify(service).getResourcesCap(SUBORGANIZATION);
+ verify(subject)
+ .hasPermission(
+ OrganizationDomain.DOMAIN_ID, PARENT_ORGANIZATION, OrganizationDomain.MANAGE_RESOURCES);
+ verify(superPrivilegesChecker).hasSuperPrivileges();
+ }
+
+ @Test
+ public void
+ shouldNotCheckManageResourcesPermissionsOnGettingResourcesCapWhenUserHasSuperPrivileges()
+ throws Exception {
+ when(superPrivilegesChecker.hasSuperPrivileges()).thenReturn(true);
+
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .expect()
+ .statusCode(200)
+ .when()
+ .get(SECURE_PATH + "/organization/resource/" + SUBORGANIZATION + "/cap");
+
+ verify(service).getResourcesCap(SUBORGANIZATION);
+ verify(subject, never())
+ .hasPermission(
+ OrganizationDomain.DOMAIN_ID, PARENT_ORGANIZATION, OrganizationDomain.MANAGE_RESOURCES);
+ verify(superPrivilegesChecker).hasSuperPrivileges();
+ }
+
+ @Test(
+ expectedExceptions = ForbiddenException.class,
+ expectedExceptionsMessageRegExp =
+ "The user does not have permission to perform this operation")
+ public void shouldThrowForbiddenExceptionWhenRequestedUnknownMethod() throws Exception {
+ final GenericResourceMethod mock = mock(GenericResourceMethod.class);
+ Method unknownMethod =
+ OrganizationResourcesDistributionService.class.getMethod("getServiceDescriptor");
+ when(mock.getMethod()).thenReturn(unknownMethod);
+
+ permissionsFilter.filter(mock, new Object[] {});
+ }
+
+ @Filter
+ public static class EnvironmentFilter implements RequestFilter {
+ @Override
+ public void doFilter(GenericContainerRequest request) {
+ EnvironmentContext.getCurrent().setSubject(subject);
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationalAccountPermissionsCheckerTest.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationalAccountPermissionsCheckerTest.java
new file mode 100644
index 00000000000..f6440097e76
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/permissions/OrganizationalAccountPermissionsCheckerTest.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.permissions;
+
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.multiuser.api.permission.server.account.AccountOperation;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link
+ * org.eclipse.che.multiuser.organization.api.permissions.OrganizationalAccountPermissionsChecker}
+ *
+ * @author Sergii Leshchenko
+ */
+@Listeners(MockitoTestNGListener.class)
+public class OrganizationalAccountPermissionsCheckerTest {
+ private static final String ORG_ID = "org123";
+
+ @Mock private Subject subject;
+
+ private OrganizationalAccountPermissionsChecker permissionsChecker;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ lenient().when(subject.hasPermission(anyString(), anyString(), anyString())).thenReturn(true);
+
+ EnvironmentContext.getCurrent().setSubject(subject);
+
+ permissionsChecker = new OrganizationalAccountPermissionsChecker();
+ }
+
+ @AfterMethod
+ public void tearDown() throws Exception {
+ EnvironmentContext.reset();
+ }
+
+ @Test
+ public void shouldReturnOrganizationalReturnType() throws Exception {
+ // then
+ assertEquals(permissionsChecker.getAccountType(), OrganizationImpl.ORGANIZATIONAL_ACCOUNT);
+ }
+
+ @Test
+ public void shouldCheckCreateWorkspacesPermissionOnOrganizationDomainLevel() throws Exception {
+ permissionsChecker.checkPermissions(ORG_ID, AccountOperation.CREATE_WORKSPACE);
+
+ verify(subject)
+ .hasPermission(OrganizationDomain.DOMAIN_ID, ORG_ID, OrganizationDomain.CREATE_WORKSPACES);
+ }
+
+ @Test(
+ expectedExceptions = ForbiddenException.class,
+ expectedExceptionsMessageRegExp =
+ "User is not authorized to create workspaces in specified namespace.")
+ public void shouldThrowForbiddenWhenUserDoesNotHavePermissionToCreateWorkspaces()
+ throws Exception {
+ when(subject.hasPermission(
+ OrganizationDomain.DOMAIN_ID, ORG_ID, OrganizationDomain.CREATE_WORKSPACES))
+ .thenReturn(false);
+
+ permissionsChecker.checkPermissions(ORG_ID, AccountOperation.CREATE_WORKSPACE);
+ }
+
+ @Test
+ public void shouldCheckManageWorkspacesPermissionOnOrganizationDomainLevel() throws Exception {
+ permissionsChecker.checkPermissions(ORG_ID, AccountOperation.MANAGE_WORKSPACES);
+
+ verify(subject)
+ .hasPermission(OrganizationDomain.DOMAIN_ID, ORG_ID, OrganizationDomain.MANAGE_WORKSPACES);
+ }
+
+ @Test(
+ expectedExceptions = ForbiddenException.class,
+ expectedExceptionsMessageRegExp = "User is not authorized to use specified namespace.")
+ public void shouldThrowForbiddenWhenUserDoesNotHavePermissionToManagerWorkspaces()
+ throws Exception {
+ when(subject.hasPermission(
+ OrganizationDomain.DOMAIN_ID, ORG_ID, OrganizationDomain.MANAGE_WORKSPACES))
+ .thenReturn(false);
+
+ permissionsChecker.checkPermissions(ORG_ID, AccountOperation.MANAGE_WORKSPACES);
+ }
+
+ @Test(dataProvider = "requiredAction")
+ public void
+ shouldNotThrowExceptionWhenUserHasAtLeastOnRequiredPermissionOnGettingResourcesInformation(
+ String action) throws Exception {
+ when(subject.hasPermission(anyString(), anyString(), anyString())).thenReturn(false);
+ when(subject.hasPermission(OrganizationDomain.DOMAIN_ID, ORG_ID, action)).thenReturn(true);
+
+ permissionsChecker.checkPermissions(ORG_ID, AccountOperation.SEE_RESOURCE_INFORMATION);
+
+ verify(subject).hasPermission(OrganizationDomain.DOMAIN_ID, ORG_ID, action);
+ }
+
+ @Test(
+ expectedExceptions = ForbiddenException.class,
+ expectedExceptionsMessageRegExp =
+ "User is not authorized to see resources information of requested organization.")
+ public void shouldThrowForbiddenWhenUserDoesNotHavePermissionToSeeResourcesInformation()
+ throws Exception {
+ when(subject.hasPermission(anyString(), anyString(), anyString())).thenReturn(false);
+
+ permissionsChecker.checkPermissions(ORG_ID, AccountOperation.SEE_RESOURCE_INFORMATION);
+ }
+
+ @DataProvider
+ private Object[][] requiredAction() {
+ return new Object[][] {
+ {OrganizationDomain.CREATE_WORKSPACES},
+ {OrganizationDomain.MANAGE_WORKSPACES},
+ {OrganizationDomain.MANAGE_RESOURCES}
+ };
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/DefaultOrganizationResourcesProviderTest.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/DefaultOrganizationResourcesProviderTest.java
new file mode 100644
index 00000000000..2d0e49bd3d7
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/DefaultOrganizationResourcesProviderTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.resource;
+
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+import org.eclipse.che.multiuser.resource.api.type.RamResourceType;
+import org.eclipse.che.multiuser.resource.api.type.RuntimeResourceType;
+import org.eclipse.che.multiuser.resource.api.type.TimeoutResourceType;
+import org.eclipse.che.multiuser.resource.api.type.WorkspaceResourceType;
+import org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link
+ * org.eclipse.che.multiuser.organization.api.resource.DefaultOrganizationResourcesProvider}
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners(MockitoTestNGListener.class)
+public class DefaultOrganizationResourcesProviderTest {
+ @Mock private OrganizationManager organizationManager;
+ @Mock private Organization organization;
+
+ private DefaultOrganizationResourcesProvider organizationResourcesProvider;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ organizationResourcesProvider =
+ new DefaultOrganizationResourcesProvider(organizationManager, "2gb", 10, 5, 10 * 60 * 1000);
+ when(organizationManager.getById(anyString())).thenReturn(organization);
+ }
+
+ @Test
+ public void shouldNotProvideDefaultResourcesForSuborganization() throws Exception {
+ // given
+ when(organization.getParent()).thenReturn("parentId");
+
+ // when
+ final List defaultResources =
+ organizationResourcesProvider.getResources("organization123");
+
+ // then
+ verify(organizationManager).getById("organization123");
+ assertTrue(defaultResources.isEmpty());
+ }
+
+ @Test
+ public void shouldProvideDefaultResourcesForRootOrganization() throws Exception {
+ // given
+ when(organization.getParent()).thenReturn(null);
+
+ // when
+ final List defaultResources =
+ organizationResourcesProvider.getResources("organization123");
+
+ // then
+ verify(organizationManager).getById("organization123");
+ assertEquals(defaultResources.size(), 4);
+ assertTrue(
+ defaultResources.contains(
+ new ResourceImpl(TimeoutResourceType.ID, 10, TimeoutResourceType.UNIT)));
+ assertTrue(
+ defaultResources.contains(
+ new ResourceImpl(RamResourceType.ID, 2048, RamResourceType.UNIT)));
+ assertTrue(
+ defaultResources.contains(
+ new ResourceImpl(WorkspaceResourceType.ID, 10, WorkspaceResourceType.UNIT)));
+ assertTrue(
+ defaultResources.contains(
+ new ResourceImpl(RuntimeResourceType.ID, 5, RuntimeResourceType.UNIT)));
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourceLockKeyProviderTest.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourceLockKeyProviderTest.java
new file mode 100644
index 00000000000..0323e632bb8
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourceLockKeyProviderTest.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.resource;
+
+import static org.mockito.Mockito.lenient;
+import static org.testng.Assert.assertEquals;
+
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link
+ * org.eclipse.che.multiuser.organization.api.resource.OrganizationResourceLockKeyProvider}
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners(MockitoTestNGListener.class)
+public class OrganizationResourceLockKeyProviderTest {
+ @Mock private OrganizationManager organizationManager;
+
+ @InjectMocks private OrganizationResourceLockKeyProvider lockProvider;
+
+ @Test
+ public void shouldReturnRootOrganizationId() throws Exception {
+ // given
+ createOrganization("root", null);
+ createOrganization("suborg", "root");
+ createOrganization("subsuborg", "suborg");
+
+ // when
+ final String lockId = lockProvider.getLockKey("subsuborg");
+
+ // then
+ assertEquals(lockId, "root");
+ }
+
+ @Test
+ public void shouldReturnOrganizationalReturnType() throws Exception {
+ // then
+ assertEquals(lockProvider.getAccountType(), OrganizationImpl.ORGANIZATIONAL_ACCOUNT);
+ }
+
+ private void createOrganization(String id, String parentId) throws Exception {
+ lenient()
+ .when(organizationManager.getById(id))
+ .thenReturn(new OrganizationImpl(id, id + "Name", parentId));
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourcesDistributionServiceTest.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourcesDistributionServiceTest.java
new file mode 100644
index 00000000000..1815b4df3f3
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourcesDistributionServiceTest.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.resource;
+
+import static io.restassured.RestAssured.given;
+import static java.util.Collections.singletonList;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD;
+import static org.everrest.assured.JettyHttpServer.SECURE_PATH;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import io.restassured.response.Response;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.stream.Collectors;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.rest.ApiExceptionMapper;
+import org.eclipse.che.api.core.rest.CheJsonProvider;
+import org.eclipse.che.api.core.rest.shared.dto.ServiceError;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.organization.shared.dto.OrganizationDistributedResourcesDto;
+import org.eclipse.che.multiuser.resource.api.free.ResourceValidator;
+import org.eclipse.che.multiuser.resource.shared.dto.ResourceDto;
+import org.everrest.assured.EverrestJetty;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link
+ * org.eclipse.che.multiuser.organization.api.resource.OrganizationResourcesDistributionService}
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners({EverrestJetty.class, MockitoTestNGListener.class})
+public class OrganizationResourcesDistributionServiceTest {
+ @SuppressWarnings("unused") // is declared for deploying by everrest-assured
+ private ApiExceptionMapper mapper;
+
+ @SuppressWarnings("unused") // is declared for deploying by everrest-assured
+ private CheJsonProvider jsonProvider = new CheJsonProvider(new HashSet<>());
+
+ @Mock private OrganizationResourcesDistributor organizationResourcesManager;
+ @Mock private ResourceValidator resourceValidator;
+
+ @InjectMocks private OrganizationResourcesDistributionService service;
+
+ @Test
+ public void shouldCapOrganizationResources() throws Exception {
+ ResourceDto resource =
+ DtoFactory.newDto(ResourceDto.class).withType("test").withAmount(1020).withUnit("unit");
+ List resources = singletonList(resource);
+
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .body(resources)
+ .when()
+ .post(SECURE_PATH + "/organization/resource/organization123/cap")
+ .then()
+ .assertThat()
+ .statusCode(204);
+
+ verify(organizationResourcesManager).capResources("organization123", resources);
+ verify(resourceValidator).validate(resource);
+ }
+
+ @Test
+ public void
+ shouldReturn400WhenBodyContainTwoResourcesWithTheSameTypeOnDistributingOrganizationResources()
+ throws Exception {
+ List resources =
+ Arrays.asList(
+ DtoFactory.newDto(ResourceDto.class).withType("test"),
+ DtoFactory.newDto(ResourceDto.class).withType("test"));
+
+ Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .body(resources)
+ .when()
+ .post(SECURE_PATH + "/organization/resource/organization123/cap");
+ assertEquals(response.statusCode(), 400);
+ String errorMessage =
+ DtoFactory.getInstance()
+ .createDtoFromJson(response.print(), ServiceError.class)
+ .getMessage();
+ assertEquals(errorMessage, "Resources to cap must contain only one resource with type 'test'.");
+ }
+
+ @Test
+ public void shouldReturnResourcesCapForSuborganization() throws Exception {
+ final ResourceDto resourcesCap =
+ DtoFactory.newDto(ResourceDto.class).withType("test").withAmount(1020).withUnit("unit");
+ final List toFetch = singletonList(resourcesCap);
+ doReturn(toFetch).when(organizationResourcesManager).getResourcesCaps(any());
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .get(SECURE_PATH + "/organization/resource/organization123/cap");
+
+ assertEquals(response.statusCode(), 200);
+ final List fetched = unwrapDtoList(response, ResourceDto.class);
+ assertEquals(fetched.size(), 1);
+ assertTrue(fetched.contains(resourcesCap));
+ verify(organizationResourcesManager).getResourcesCaps("organization123");
+ }
+
+ @Test
+ public void shouldReturnOrganizationDistributedResources() throws Exception {
+ final OrganizationDistributedResourcesDto distributedResources =
+ createOrganizationDistributedResources();
+ final List toFetch = singletonList(distributedResources);
+ doReturn(new Page<>(toFetch, 1, 1, 3))
+ .when(organizationResourcesManager)
+ .getByParent(any(), anyInt(), anyLong());
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .get(SECURE_PATH + "/organization/resource/organization123?maxItems=1&skipCount=1");
+ assertEquals(response.statusCode(), 200);
+ final List fetched =
+ unwrapDtoList(response, OrganizationDistributedResourcesDto.class);
+ assertEquals(fetched.size(), 1);
+ assertTrue(fetched.contains(distributedResources));
+ verify(organizationResourcesManager).getByParent("organization123", 1, 1L);
+ }
+
+ private static List unwrapDtoList(Response response, Class dtoClass) {
+ return DtoFactory.getInstance()
+ .createListDtoFromJson(response.body().print(), dtoClass)
+ .stream()
+ .collect(Collectors.toList());
+ }
+
+ private OrganizationDistributedResourcesDto createOrganizationDistributedResources() {
+ return DtoFactory.newDto(OrganizationDistributedResourcesDto.class)
+ .withOrganizationId("organization123")
+ .withResourcesCap(
+ singletonList(
+ DtoFactory.newDto(ResourceDto.class)
+ .withType("test")
+ .withAmount(1020)
+ .withUnit("unit")));
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourcesDistributorTest.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourcesDistributorTest.java
new file mode 100644
index 00000000000..21f7b8ebfbb
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationResourcesDistributorTest.java
@@ -0,0 +1,248 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.resource;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doCallRealMethod;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+import java.util.Collections;
+import java.util.List;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.commons.lang.concurrent.Unlocker;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.shared.model.OrganizationDistributedResources;
+import org.eclipse.che.multiuser.organization.spi.OrganizationDistributedResourcesDao;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationDistributedResourcesImpl;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+import org.eclipse.che.multiuser.resource.api.ResourceAggregator;
+import org.eclipse.che.multiuser.resource.api.exception.NoEnoughResourcesException;
+import org.eclipse.che.multiuser.resource.api.usage.ResourceManager;
+import org.eclipse.che.multiuser.resource.api.usage.ResourcesLocks;
+import org.eclipse.che.multiuser.resource.model.Resource;
+import org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link OrganizationResourcesDistributor}
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners(MockitoTestNGListener.class)
+public class OrganizationResourcesDistributorTest {
+ private static final String PARENT_ORG_ID = "parentOrg123";
+ private static final String ORG_ID = "organization123";
+
+ @Mock private Unlocker lock;
+ @Mock private OrganizationDistributedResourcesDao distributedResourcesDao;
+ @Mock private ResourcesLocks resourcesLocks;
+ @Mock private ResourceManager resourceManager;
+ @Mock private ResourceAggregator resourceAggregator;
+ @Mock private OrganizationManager organizationManager;
+
+ @Spy @InjectMocks private OrganizationResourcesDistributor manager;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ lenient().doNothing().when(manager).checkResourcesAvailability(anyString(), any());
+ lenient().when(resourcesLocks.lock(anyString())).thenReturn(lock);
+
+ lenient()
+ .when(organizationManager.getById(ORG_ID))
+ .thenReturn(new OrganizationImpl(ORG_ID, ORG_ID + "name", PARENT_ORG_ID));
+ lenient()
+ .when(organizationManager.getById(PARENT_ORG_ID))
+ .thenReturn(new OrganizationImpl(PARENT_ORG_ID, PARENT_ORG_ID + "name", null));
+ }
+
+ @Test
+ public void shouldCapResources() throws Exception {
+ List toCap = singletonList(createTestResource(1000));
+
+ // when
+ manager.capResources(ORG_ID, toCap);
+
+ // then
+ verify(manager).checkResourcesAvailability(ORG_ID, toCap);
+ verify(distributedResourcesDao).store(new OrganizationDistributedResourcesImpl(ORG_ID, toCap));
+ verify(resourcesLocks).lock(ORG_ID);
+ verify(lock).close();
+ }
+
+ @Test
+ public void shouldRemoveResourceFromListWhenItsAmountEqualsToMinusOne() throws Exception {
+ ResourceImpl toCap = new ResourceImpl("test1", 1000, "init");
+ ResourceImpl toReset = new ResourceImpl("test2", -1, "init");
+ List resourcesToCap = asList(toCap, toReset);
+
+ // when
+ manager.capResources(ORG_ID, resourcesToCap);
+
+ // then
+ verify(manager).checkResourcesAvailability(ORG_ID, singletonList(toCap));
+ verify(distributedResourcesDao)
+ .store(new OrganizationDistributedResourcesImpl(ORG_ID, singletonList(toCap)));
+ verify(resourcesLocks).lock(ORG_ID);
+ verify(lock).close();
+ }
+
+ @Test
+ public void shouldRemoveResourcesCapWhenInvokeCapWithEmptyList() throws Exception {
+ // when
+ manager.capResources(ORG_ID, Collections.emptyList());
+
+ // then
+ verify(manager, never()).checkResourcesAvailability(anyString(), any());
+ verify(distributedResourcesDao).remove(ORG_ID);
+ verify(resourcesLocks).lock(ORG_ID);
+ verify(lock).close();
+ }
+
+ @Test(
+ expectedExceptions = ConflictException.class,
+ expectedExceptionsMessageRegExp = "It is not allowed to cap resources for root organization.")
+ public void shouldThrowConflictExceptionOnCappingResourcesForRootOrganization() throws Exception {
+ // when
+ manager.capResources(PARENT_ORG_ID, Collections.emptyList());
+ }
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void shouldThrowNpeOnDistributionResourcesWithNullOrganizationId() throws Exception {
+ // when
+ manager.capResources(null, emptyList());
+ }
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void shouldThrowNpeOnDistributionNullResourcesList() throws Exception {
+ // when
+ manager.capResources(ORG_ID, null);
+ }
+
+ @Test
+ public void shouldGetDistributedResources() throws Exception {
+ // given
+ final OrganizationDistributedResourcesImpl distributedResources =
+ createDistributedResources(1000);
+ doReturn(new Page<>(singletonList(distributedResources), 0, 10, 1))
+ .when(distributedResourcesDao)
+ .getByParent(anyString(), anyInt(), anyLong());
+
+ // when
+ final Page extends OrganizationDistributedResources> fetchedDistributedResources =
+ manager.getByParent(ORG_ID, 10, 0);
+
+ // then
+ assertEquals(fetchedDistributedResources.getTotalItemsCount(), 1);
+ assertEquals(fetchedDistributedResources.getItems().get(0), distributedResources);
+ verify(distributedResourcesDao).getByParent(ORG_ID, 10, 0);
+ }
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void shouldThrowNpeOnGettingDistributedResourcesByNullOrganizationId() throws Exception {
+ // when
+ manager.getByParent(null, 10, 10);
+ }
+
+ @Test
+ public void shouldGetResourcesCap() throws Exception {
+ // given
+ final OrganizationDistributedResourcesImpl distributedResources =
+ createDistributedResources(1000);
+ when(distributedResourcesDao.get(anyString())).thenReturn(distributedResources);
+
+ // when
+ final List extends Resource> fetchedDistributedResources = manager.getResourcesCaps(ORG_ID);
+
+ // then
+ assertEquals(fetchedDistributedResources, distributedResources.getResourcesCap());
+ verify(distributedResourcesDao).get(ORG_ID);
+ }
+
+ @Test(expectedExceptions = NullPointerException.class)
+ public void shouldThrowNpeOnGettingResourcesCapByNullOrganizationId() throws Exception {
+ // when
+ manager.getResourcesCaps(null);
+ }
+
+ @Test
+ public void shouldResourceAvailabilityCappingResourcesWhenResourceCapIsLessThanUsedOne()
+ throws Exception {
+ // given
+ doCallRealMethod().when(manager).checkResourcesAvailability(anyString(), any());
+
+ ResourceImpl used = createTestResource(500);
+ doReturn(singletonList(used)).when(resourceManager).getUsedResources(any());
+
+ ResourceImpl toCap = createTestResource(700);
+ doReturn(createTestResource(200)).when(resourceAggregator).deduct((Resource) any(), any());
+
+ // when
+ manager.checkResourcesAvailability(ORG_ID, singletonList(toCap));
+
+ // then
+ verify(resourceManager).getUsedResources(ORG_ID);
+ verify(resourceAggregator).deduct(toCap, used);
+ }
+
+ @Test(
+ expectedExceptions = ConflictException.class,
+ expectedExceptionsMessageRegExp = "Resources are currently in use. Denied.")
+ public void shouldResourceAvailabilityCappingResourcesWhenResourceCapIsGreaterThanUsedOne()
+ throws Exception {
+ // given
+ doCallRealMethod().when(manager).checkResourcesAvailability(anyString(), any());
+ doReturn("Denied.").when(manager).getMessage(anyString());
+
+ ResourceImpl used = createTestResource(1000);
+ doReturn(singletonList(used)).when(resourceManager).getUsedResources(any());
+
+ ResourceImpl toCap = createTestResource(700);
+ doThrow(new NoEnoughResourcesException(emptyList(), emptyList(), singletonList(toCap)))
+ .when(resourceAggregator)
+ .deduct((Resource) any(), any());
+
+ // when
+ manager.checkResourcesAvailability(ORG_ID, singletonList(toCap));
+
+ // then
+ verify(resourceManager).getUsedResources(ORG_ID);
+ verify(resourceAggregator).deduct(toCap, used);
+ }
+
+ private ResourceImpl createTestResource(long amount) {
+ return new ResourceImpl("test", amount, "init");
+ }
+
+ private OrganizationDistributedResourcesImpl createDistributedResources(long resourceAmount) {
+ return new OrganizationDistributedResourcesImpl(
+ ORG_ID, singletonList(createTestResource(resourceAmount)));
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationalAccountAvailableResourcesProviderTest.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationalAccountAvailableResourcesProviderTest.java
new file mode 100644
index 00000000000..79e2d2e0c7a
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/OrganizationalAccountAvailableResourcesProviderTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.resource;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+import javax.inject.Provider;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+import org.eclipse.che.multiuser.resource.api.ResourceAggregator;
+import org.eclipse.che.multiuser.resource.api.exception.NoEnoughResourcesException;
+import org.eclipse.che.multiuser.resource.api.usage.ResourceManager;
+import org.eclipse.che.multiuser.resource.model.Resource;
+import org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Spy;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/** Test for {@link OrganizationalAccountAvailableResourcesProvider} */
+@Listeners(MockitoTestNGListener.class)
+public class OrganizationalAccountAvailableResourcesProviderTest {
+ private static final String ROOT_ORG_NAME = "root";
+ private static final String ROOT_ORG_ID = "organization123";
+ private static final String SUBORG_ID = "organization321";
+ private static final String SUBSUBORG_ID = "organization231";
+
+ @Mock private Provider resourceManagerProvider;
+ @Mock private ResourceManager resourceManager;
+ @Mock private ResourceAggregator resourceAggregator;
+ @Mock private OrganizationManager organizationManager;
+
+ @InjectMocks @Spy
+ private OrganizationalAccountAvailableResourcesProvider availableResourcesProvider;
+
+ private Organization rootOrganization;
+ private Organization suborganization;
+ private Organization subsuborganization;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ lenient().when(resourceManagerProvider.get()).thenReturn(resourceManager);
+
+ rootOrganization = new OrganizationImpl(ROOT_ORG_ID, ROOT_ORG_NAME, null);
+ suborganization = new OrganizationImpl(SUBORG_ID, "root/suborg", ROOT_ORG_ID);
+ subsuborganization = new OrganizationImpl(SUBSUBORG_ID, "root/suborg/subsuborg", SUBORG_ID);
+
+ lenient().when(organizationManager.getById(ROOT_ORG_ID)).thenReturn(rootOrganization);
+ lenient().when(organizationManager.getById(SUBORG_ID)).thenReturn(suborganization);
+ lenient().when(organizationManager.getById(SUBSUBORG_ID)).thenReturn(subsuborganization);
+ }
+
+ @Test
+ public void shouldReturnAvailableResourcesForRootOrganization() throws Exception {
+ // given
+ ResourceImpl availableResource = new ResourceImpl("test", 5000, "unit");
+ doReturn(singletonList(availableResource))
+ .when(availableResourcesProvider)
+ .getAvailableOrganizationResources(any());
+
+ // when
+ List extends Resource> availableResources =
+ availableResourcesProvider.getAvailableResources(ROOT_ORG_ID);
+
+ // then
+ assertEquals(availableResources.size(), 1);
+ assertEquals(availableResources.get(0), availableResource);
+ verify(availableResourcesProvider).getAvailableResources(ROOT_ORG_ID);
+ }
+
+ @Test
+ public void shouldReturnAvailableResourcesForSuborganization() throws Exception {
+ // given
+ ResourceImpl parentAvailableResource = new ResourceImpl("test", 3000, "unit");
+ prepareAvailableResource(ROOT_ORG_ID, parentAvailableResource);
+ ResourceImpl suborgAvailableResource = new ResourceImpl("test", 5000, "unit");
+ prepareAvailableResource(SUBORG_ID, suborgAvailableResource);
+ doReturn(asList(parentAvailableResource, suborgAvailableResource))
+ .when(resourceAggregator)
+ .intersection(anyList(), anyList());
+ doReturn(singletonList(parentAvailableResource)).when(resourceAggregator).min(anyList());
+
+ // when
+ List extends Resource> availableResources =
+ availableResourcesProvider.getAvailableResources(SUBORG_ID);
+
+ // then
+ assertEquals(availableResources.size(), 1);
+ assertEquals(availableResources.get(0), parentAvailableResource);
+ verify(availableResourcesProvider).getAvailableOrganizationResources(rootOrganization);
+ verify(availableResourcesProvider).getAvailableOrganizationResources(suborganization);
+ verify(resourceAggregator)
+ .intersection(
+ singletonList(parentAvailableResource), singletonList(suborgAvailableResource));
+ verify(resourceAggregator).min(asList(parentAvailableResource, suborgAvailableResource));
+ }
+
+ @Test
+ public void shouldReturnAvailableResourcesAsTotalMinusUsedByItselfAndItsSuborganizations()
+ throws Exception {
+ // given
+ ResourceImpl totalResource = new ResourceImpl("test", 9000, "unit");
+ doReturn(singletonList(totalResource)).when(resourceManager).getTotalResources(anyString());
+
+ ResourceImpl usedResource = new ResourceImpl("test", 3000, "unit");
+ doReturn(singletonList(usedResource)).when(resourceManager).getUsedResources(anyString());
+
+ ResourceImpl usedBySuborgResource = new ResourceImpl("test", 1500, "unit");
+ ResourceImpl usedBySubsuborgResource = new ResourceImpl("test", 2000, "unit");
+ doReturn(asList(usedBySuborgResource, usedBySubsuborgResource))
+ .when(availableResourcesProvider)
+ .getUsedResourcesBySuborganizations(anyString());
+
+ ResourceImpl availableResource = new ResourceImpl("test", 2500, "unit");
+ doReturn(singletonList(availableResource))
+ .when(resourceAggregator)
+ .deduct(anyList(), anyList());
+
+ // when
+ List extends Resource> availableResources =
+ availableResourcesProvider.getAvailableOrganizationResources(rootOrganization);
+
+ // then
+ assertEquals(availableResources.size(), 1);
+ assertEquals(availableResources.get(0), availableResource);
+ verify(resourceManager).getTotalResources(ROOT_ORG_ID);
+ verify(resourceManager).getUsedResources(ROOT_ORG_ID);
+ verify(availableResourcesProvider).getUsedResourcesBySuborganizations(ROOT_ORG_NAME);
+ verify(resourceAggregator)
+ .deduct(
+ singletonList(totalResource),
+ asList(usedResource, usedBySuborgResource, usedBySubsuborgResource));
+ }
+
+ @Test
+ public void shouldReturnExcessiveResourcesWhenUsedResourceAreGreaterThanTotal() throws Exception {
+ // given
+ ResourceImpl totalResource = new ResourceImpl("test", 9000, "unit");
+ ResourceImpl excessiveTotalResource = new ResourceImpl("test1", 1000, "unit");
+ doReturn(asList(totalResource, excessiveTotalResource))
+ .when(resourceManager)
+ .getTotalResources(anyString());
+
+ ResourceImpl usedResource = new ResourceImpl("test", 10000, "unit");
+ doReturn(singletonList(usedResource)).when(resourceManager).getUsedResources(anyString());
+
+ doReturn(emptyList())
+ .when(availableResourcesProvider)
+ .getUsedResourcesBySuborganizations(anyString());
+
+ doThrow(new NoEnoughResourcesException(emptyList(), emptyList(), emptyList()))
+ .when(resourceAggregator)
+ .deduct(anyList(), anyList());
+ doReturn(singletonList(excessiveTotalResource))
+ .when(resourceAggregator)
+ .excess(anyList(), anyList());
+
+ // when
+ List extends Resource> availableResources =
+ availableResourcesProvider.getAvailableOrganizationResources(rootOrganization);
+
+ // then
+ assertEquals(availableResources.size(), 1);
+ assertEquals(availableResources.get(0), excessiveTotalResource);
+ verify(resourceManager).getTotalResources(ROOT_ORG_ID);
+ verify(resourceManager).getUsedResources(ROOT_ORG_ID);
+ verify(availableResourcesProvider).getUsedResourcesBySuborganizations(ROOT_ORG_NAME);
+ verify(resourceAggregator)
+ .deduct(asList(totalResource, excessiveTotalResource), singletonList(usedResource));
+ verify(resourceAggregator)
+ .excess(asList(totalResource, excessiveTotalResource), singletonList(usedResource));
+ }
+
+ @Test
+ public void shouldCalculateUsedResourceBySuborganizations() throws Exception {
+ // given
+ doReturn(new Page<>(singletonList(suborganization), 0, 1, 2))
+ .doReturn(new Page<>(singletonList(subsuborganization), 1, 1, 2))
+ .when(organizationManager)
+ .getSuborganizations(anyString(), anyInt(), anyLong());
+ ResourceImpl usedBySuborgResource = new ResourceImpl("test", 1500, "unit");
+ doReturn(singletonList(usedBySuborgResource)).when(resourceManager).getUsedResources(SUBORG_ID);
+ ResourceImpl usedBySubsuborgResource = new ResourceImpl("test", 2000, "unit");
+ doReturn(singletonList(usedBySubsuborgResource))
+ .when(resourceManager)
+ .getUsedResources(SUBSUBORG_ID);
+
+ // when
+ List extends Resource> usedResources =
+ availableResourcesProvider.getUsedResourcesBySuborganizations(ROOT_ORG_NAME);
+
+ // then
+ assertEquals(usedResources.size(), 2);
+ assertTrue(usedResources.contains(usedBySuborgResource));
+ assertTrue(usedResources.contains(usedBySubsuborgResource));
+ verify(organizationManager, times(2))
+ .getSuborganizations(eq(ROOT_ORG_NAME), anyInt(), anyLong());
+ verify(resourceManager).getUsedResources(SUBORG_ID);
+ verify(resourceManager).getUsedResources(SUBSUBORG_ID);
+ }
+
+ private void prepareAvailableResource(String organizationId, ResourceImpl availableResource)
+ throws NotFoundException, ServerException {
+ doReturn(singletonList(availableResource))
+ .when(availableResourcesProvider)
+ .getAvailableOrganizationResources(
+ argThat(argument -> organizationId.equals(((Organization) argument).getId())));
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/SuborganizationResourcesProviderTest.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/SuborganizationResourcesProviderTest.java
new file mode 100644
index 00000000000..7e3281f1695
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/api/resource/SuborganizationResourcesProviderTest.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.api.resource;
+
+import static java.util.Arrays.asList;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertTrue;
+
+import java.util.List;
+import javax.inject.Provider;
+import org.eclipse.che.account.api.AccountManager;
+import org.eclipse.che.account.shared.model.Account;
+import org.eclipse.che.multiuser.organization.api.OrganizationManager;
+import org.eclipse.che.multiuser.organization.shared.model.Organization;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+import org.eclipse.che.multiuser.resource.api.usage.ResourceManager;
+import org.eclipse.che.multiuser.resource.model.ProvidedResources;
+import org.eclipse.che.multiuser.resource.spi.impl.ProvidedResourcesImpl;
+import org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link
+ * org.eclipse.che.multiuser.organization.api.resource.SuborganizationResourcesProvider}
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners(MockitoTestNGListener.class)
+public class SuborganizationResourcesProviderTest {
+ @Mock private Account account;
+ @Mock private Organization organization;
+
+ @Mock private AccountManager accountManager;
+ @Mock private OrganizationManager organizationManager;
+ @Mock private OrganizationResourcesDistributor resourcesDistributor;
+ @Mock private Provider distributorProvider;
+ @Mock private Provider resourceManagerProvider;
+ @Mock private ResourceManager resourceManager;
+
+ private SuborganizationResourcesProvider suborganizationResourcesProvider;
+
+ @BeforeMethod
+ public void setUp() throws Exception {
+ when(accountManager.getById(any())).thenReturn(account);
+ lenient().when(organizationManager.getById(any())).thenReturn(organization);
+
+ lenient().when(distributorProvider.get()).thenReturn(resourcesDistributor);
+
+ lenient().when(resourceManagerProvider.get()).thenReturn(resourceManager);
+
+ suborganizationResourcesProvider =
+ new SuborganizationResourcesProvider(
+ accountManager, organizationManager, distributorProvider, resourceManagerProvider);
+ }
+
+ @Test
+ public void shouldNotProvideResourcesForNonOrganizationalAccounts() throws Exception {
+ // given
+ when(account.getType()).thenReturn("test");
+
+ // when
+ final List providedResources =
+ suborganizationResourcesProvider.getResources("account123");
+
+ // then
+ assertTrue(providedResources.isEmpty());
+ verify(accountManager).getById("account123");
+ }
+
+ @Test
+ public void shouldNotProvideResourcesForRootOrganizationalAccount() throws Exception {
+ // given
+ when(account.getType()).thenReturn(OrganizationImpl.ORGANIZATIONAL_ACCOUNT);
+ when(organization.getParent()).thenReturn(null);
+
+ // when
+ final List providedResources =
+ suborganizationResourcesProvider.getResources("organization123");
+
+ // then
+ assertTrue(providedResources.isEmpty());
+ verify(accountManager).getById("organization123");
+ verify(organizationManager).getById("organization123");
+ }
+
+ @Test
+ public void shouldProvideResourcesForSuborganizationalAccount() throws Exception {
+ // given
+ when(account.getType()).thenReturn(OrganizationImpl.ORGANIZATIONAL_ACCOUNT);
+ when(organization.getParent()).thenReturn("parentOrg");
+ final ResourceImpl parentNotCapedResource = new ResourceImpl("test", 1234, "unit");
+ final ResourceImpl parentCapedResource = new ResourceImpl("caped", 20, "unit");
+ final ResourceImpl parentUnlimitedCapedResource = new ResourceImpl("unlimited", -1, "unit");
+ doReturn(asList(parentNotCapedResource, parentCapedResource, parentUnlimitedCapedResource))
+ .when(resourceManager)
+ .getTotalResources(anyString());
+
+ final ResourceImpl capedResourceCap = new ResourceImpl("caped", 10, "unit");
+ final ResourceImpl unlimitedCapedResourceCap = new ResourceImpl("unlimited", 40, "unit");
+ doReturn(asList(capedResourceCap, unlimitedCapedResourceCap))
+ .when(resourcesDistributor)
+ .getResourcesCaps(any());
+
+ // when
+ final List providedResources =
+ suborganizationResourcesProvider.getResources("organization123");
+
+ // then
+ assertEquals(providedResources.size(), 1);
+ assertEquals(
+ providedResources.get(0),
+ new ProvidedResourcesImpl(
+ SuborganizationResourcesProvider.PARENT_RESOURCES_PROVIDER,
+ null,
+ "organization123",
+ -1L,
+ -1L,
+ asList(parentNotCapedResource, capedResourceCap, unlimitedCapedResourceCap)));
+ verify(accountManager).getById("organization123");
+ verify(organizationManager).getById("organization123");
+ verify(resourcesDistributor).getResourcesCaps("organization123");
+ verify(resourceManager).getTotalResources("parentOrg");
+ }
+
+ @Test
+ public void shouldNotProvideResourcesForOrganizationalAccountIfItDoesNotHaveDistributedResources()
+ throws Exception {
+ // given
+ when(account.getType()).thenReturn(OrganizationImpl.ORGANIZATIONAL_ACCOUNT);
+ when(organization.getParent()).thenReturn("parentOrg");
+
+ // when
+ final List providedResources =
+ suborganizationResourcesProvider.getResources("organization123");
+
+ // then
+ assertTrue(providedResources.isEmpty());
+ verify(accountManager).getById("organization123");
+ verify(organizationManager).getById("organization123");
+ verify(resourcesDistributor, never()).getResourcesCaps("organization123");
+ verify(resourceManager).getTotalResources("parentOrg");
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/spi/jpa/JpaOrganizationImplTckRepository.java b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/spi/jpa/JpaOrganizationImplTckRepository.java
new file mode 100644
index 00000000000..e6c73a061b5
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/java/org/eclipse/che/multiuser/organization/spi/jpa/JpaOrganizationImplTckRepository.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.organization.spi.jpa;
+
+import com.google.inject.Inject;
+import com.google.inject.persist.UnitOfWork;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import javax.inject.Provider;
+import javax.persistence.EntityManager;
+import javax.persistence.NoResultException;
+import org.eclipse.che.commons.test.tck.repository.JpaTckRepository;
+import org.eclipse.che.commons.test.tck.repository.TckRepositoryException;
+import org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl;
+
+/**
+ * Organizations require to have own repository because it is important to delete organization in
+ * reverse order that they were stored. It allows to resolve problems with removing suborganization
+ * before parent organization removing.
+ *
+ * @author Sergii Leschenko
+ */
+public class JpaOrganizationImplTckRepository extends JpaTckRepository {
+ @Inject protected Provider managerProvider;
+
+ @Inject protected UnitOfWork uow;
+
+ private final List createdOrganizations = new ArrayList<>();
+
+ public JpaOrganizationImplTckRepository() {
+ super(OrganizationImpl.class);
+ }
+
+ @Override
+ public void createAll(Collection extends OrganizationImpl> entities)
+ throws TckRepositoryException {
+ super.createAll(entities);
+ // It's important to save organization to remove them in the reverse order
+ createdOrganizations.addAll(entities);
+ }
+
+ @Override
+ public void removeAll() throws TckRepositoryException {
+ uow.begin();
+ final EntityManager manager = managerProvider.get();
+ try {
+ manager.getTransaction().begin();
+
+ for (int i = createdOrganizations.size() - 1; i > -1; i--) {
+ // The query 'DELETE FROM ....' won't be correct as it will ignore orphanRemoval
+ // and may also ignore some configuration options, while EntityManager#remove won't
+ try {
+ final OrganizationImpl organizationToRemove =
+ manager
+ .createQuery(
+ "SELECT o FROM Organization o " + "WHERE o.id = :id", OrganizationImpl.class)
+ .setParameter("id", createdOrganizations.get(i).getId())
+ .getSingleResult();
+ manager.remove(organizationToRemove);
+ } catch (NoResultException ignored) {
+ // it is already removed
+ }
+ }
+ createdOrganizations.clear();
+
+ manager.getTransaction().commit();
+ } catch (RuntimeException x) {
+ if (manager.getTransaction().isActive()) {
+ manager.getTransaction().rollback();
+ }
+ throw new TckRepositoryException(x.getLocalizedMessage(), x);
+ } finally {
+ uow.end();
+ }
+
+ // remove all objects that was created in tests
+ super.removeAll();
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/resources/META-INF/persistence.xml b/multiuser/api/che-multiuser-api-organization/src/test/resources/META-INF/persistence.xml
new file mode 100644
index 00000000000..c9409c32cad
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/resources/META-INF/persistence.xml
@@ -0,0 +1,38 @@
+
+
+
+ org.eclipse.persistence.jpa.PersistenceProvider
+ org.eclipse.che.account.spi.AccountImpl
+ org.eclipse.che.api.user.server.model.impl.UserImpl
+ org.eclipse.che.multiuser.organization.spi.impl.OrganizationImpl
+ org.eclipse.che.multiuser.organization.spi.impl.MemberImpl
+ org.eclipse.che.multiuser.organization.spi.impl.OrganizationDistributedResourcesImpl
+ org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/resources/META-INF/services/org.eclipse.che.commons.test.tck.TckModule b/multiuser/api/che-multiuser-api-organization/src/test/resources/META-INF/services/org.eclipse.che.commons.test.tck.TckModule
new file mode 100644
index 00000000000..ad7b8c2b406
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/resources/META-INF/services/org.eclipse.che.commons.test.tck.TckModule
@@ -0,0 +1 @@
+org.eclipse.che.multiuser.organization.spi.tck.jpa.OrganizationJpaTckModule
diff --git a/multiuser/api/che-multiuser-api-organization/src/test/resources/logback-test.xml b/multiuser/api/che-multiuser-api-organization/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..3d4d10adab1
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-organization/src/test/resources/logback-test.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+ %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n
+
+
+
+
+
+
+
diff --git a/multiuser/api/che-multiuser-api-permission-shared/pom.xml b/multiuser/api/che-multiuser-api-permission-shared/pom.xml
new file mode 100644
index 00000000000..e365a7f7bd2
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission-shared/pom.xml
@@ -0,0 +1,35 @@
+
+
+
+ 4.0.0
+
+ che-multiuser-api
+ org.eclipse.che.multiuser
+ 7.103.0-SNAPSHOT
+
+ che-multiuser-api-permission-shared
+ jar
+ Che Multiuser :: Permissions :: Shared
+
+
+ org.eclipse.che.core
+ che-core-api-dto
+
+
+ org.eclipse.che.core
+ che-core-commons-annotations
+
+
+
diff --git a/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/dto/DomainDto.java b/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/dto/DomainDto.java
new file mode 100644
index 00000000000..9df05ac4bc3
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/dto/DomainDto.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.shared.dto;
+
+import java.util.List;
+import org.eclipse.che.dto.shared.DTO;
+import org.eclipse.che.multiuser.api.permission.shared.model.PermissionsDomain;
+
+/** @author Sergii Leschenko */
+@DTO
+public interface DomainDto extends PermissionsDomain {
+ @Override
+ String getId();
+
+ void setId(String id);
+
+ DomainDto withId(String id);
+
+ @Override
+ List getAllowedActions();
+
+ void setAllowedActions(List allowedActions);
+
+ DomainDto withAllowedActions(List allowedActions);
+
+ @Override
+ Boolean isInstanceRequired();
+
+ void setInstanceRequired(Boolean isInstanceRequired);
+
+ DomainDto withInstanceRequired(Boolean isInstanceRequired);
+}
diff --git a/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/dto/PermissionsDto.java b/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/dto/PermissionsDto.java
new file mode 100644
index 00000000000..e9f008142a3
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/dto/PermissionsDto.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.shared.dto;
+
+import java.util.List;
+import org.eclipse.che.dto.shared.DTO;
+import org.eclipse.che.multiuser.api.permission.shared.model.Permissions;
+
+/** @author Sergii Leschenko */
+@DTO
+public interface PermissionsDto extends Permissions {
+ @Override
+ String getUserId();
+
+ void setUserId(String userId);
+
+ PermissionsDto withUserId(String userId);
+
+ @Override
+ String getDomainId();
+
+ void setDomainId(String domainId);
+
+ PermissionsDto withDomainId(String domainId);
+
+ @Override
+ String getInstanceId();
+
+ void setInstanceId(String instanceId);
+
+ PermissionsDto withInstanceId(String instanceId);
+
+ @Override
+ List getActions();
+
+ void setActions(List actions);
+
+ PermissionsDto withActions(List actions);
+}
diff --git a/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/event/EventType.java b/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/event/EventType.java
new file mode 100644
index 00000000000..8f052e8b000
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/event/EventType.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.shared.event;
+
+/**
+ * Defines list of event types related to permissions.
+ *
+ * @author Anton Korneta
+ */
+public enum EventType {
+ PERMISSIONS_ADDED,
+
+ PERMISSIONS_REMOVED
+}
diff --git a/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/event/PermissionsEvent.java b/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/event/PermissionsEvent.java
new file mode 100644
index 00000000000..f667564e1ae
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/event/PermissionsEvent.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.shared.event;
+
+import org.eclipse.che.commons.annotation.Nullable;
+import org.eclipse.che.multiuser.api.permission.shared.model.Permissions;
+
+/**
+ * The base interface for all events related to permissions.
+ *
+ * @author Anton Korneta
+ */
+public interface PermissionsEvent {
+
+ /** Returns the permissions related to this event. */
+ Permissions getPermissions();
+
+ /** Returns concrete event type, see {@link EventType}. */
+ EventType getType();
+
+ /** Returns name of user who acted with permission or null if user is undefined. */
+ @Nullable
+ String getInitiator();
+}
diff --git a/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/model/Permissions.java b/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/model/Permissions.java
new file mode 100644
index 00000000000..9554c9d4f6e
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/model/Permissions.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.shared.model;
+
+import java.util.List;
+import org.eclipse.che.commons.annotation.Nullable;
+
+/**
+ * Represents users' permissions to access to some resources
+ *
+ * @author Sergii Leschenko
+ */
+public interface Permissions {
+ /**
+ * Returns user id
+ *
+ *
Note: also supported '*' for marking all users
+ */
+ String getUserId();
+
+ /** Returns domain id */
+ String getDomainId();
+
+ /**
+ * Returns instance id. It is optional and can be null if domain supports it
+ *
+ * @see PermissionsDomain#isInstanceRequired()
+ */
+ @Nullable
+ String getInstanceId();
+
+ /** List of actions which user can perform for particular instance */
+ List getActions();
+}
diff --git a/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/model/PermissionsDomain.java b/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/model/PermissionsDomain.java
new file mode 100644
index 00000000000..33868102266
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission-shared/src/main/java/org/eclipse/che/multiuser/api/permission/shared/model/PermissionsDomain.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.shared.model;
+
+import java.util.List;
+
+/**
+ * Describes permissions domain
+ *
+ * @author Sergii Leschenko
+ * @author gazarenkov
+ */
+public interface PermissionsDomain {
+ /** @return id of permissions domain */
+ String getId();
+
+ /** @return true if domain requires non nullable value for instance field or false otherwise */
+ Boolean isInstanceRequired();
+
+ /** @return list actions which are allowed for domain */
+ List getAllowedActions();
+}
diff --git a/multiuser/api/che-multiuser-api-permission/pom.xml b/multiuser/api/che-multiuser-api-permission/pom.xml
new file mode 100644
index 00000000000..288a176b1ca
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/pom.xml
@@ -0,0 +1,283 @@
+
+
+
+ 4.0.0
+
+ che-multiuser-api
+ org.eclipse.che.multiuser
+ 7.103.0-SNAPSHOT
+
+ che-multiuser-api-permission
+ jar
+ Che Multiuser :: Permissions API
+
+ ${project.build.directory}/generated-sources/dto/
+
+
+
+ com.google.code.gson
+ gson
+
+
+ com.google.guava
+ guava
+
+
+ com.google.inject
+ guice
+
+
+ io.swagger.core.v3
+ swagger-annotations-jakarta
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+
+
+ org.eclipse.che.core
+ che-core-api-core
+
+
+ org.eclipse.che.core
+ che-core-api-dto
+
+
+ org.eclipse.che.core
+ che-core-api-user
+
+
+ org.eclipse.che.core
+ che-core-commons-annotations
+
+
+ org.eclipse.che.core
+ che-core-commons-lang
+
+
+ org.eclipse.che.core
+ che-core-commons-test
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-permission-shared
+
+
+ org.everrest
+ everrest-core
+
+
+ com.google.inject.extensions
+ guice-persist
+ provided
+
+
+ org.eclipse.persistence
+ jakarta.persistence
+ provided
+
+
+ ch.qos.logback
+ logback-classic
+ test
+
+
+ com.h2database
+ h2
+ test
+
+
+ commons-fileupload
+ commons-fileupload
+ test
+
+
+ io.rest-assured
+ rest-assured
+ test
+
+
+ org.eclipse.che.core
+ che-core-api-account
+ test
+
+
+ org.eclipse.che.core
+ che-core-sql-schema
+ test
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-sql-schema
+ test
+
+
+ org.eclipse.persistence
+ org.eclipse.persistence.core
+ test
+
+
+ org.eclipse.persistence
+ org.eclipse.persistence.jpa
+ test
+
+
+ org.everrest
+ everrest-assured
+ test
+
+
+ org.flywaydb
+ flyway-core
+ test
+
+
+ org.hamcrest
+ hamcrest
+ test
+
+
+ org.mockito
+ mockito-core
+ test
+
+
+ org.mockito
+ mockito-testng
+ test
+
+
+ org.testng
+ testng
+ test
+
+
+
+
+
+ org.eclipse.che.core
+ che-core-api-dto-maven-plugin
+ ${project.version}
+
+
+ process-sources
+
+ generate
+
+
+
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-permission-shared
+ ${project.version}
+
+
+
+
+ org.eclipse.che.multiuser.api.permission.shared.dto
+
+ ${dto-generator-out-directory}
+ org.eclipse.che.api.multiuser.permission.server.dto.DtoServerImpls
+ server
+
+
+
+ maven-compiler-plugin
+
+
+ pre-compile
+ generate-sources
+
+ compile
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ add-domain
+ process-sources
+
+ add-resource
+
+
+
+
+ ${dto-generator-out-directory}/META-INF
+ META-INF
+
+
+
+
+
+ add-source
+ process-sources
+
+ add-source
+
+
+
+ ${dto-generator-out-directory}
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ test-jar
+
+
+
+ **/spi/tck/*.*
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ resource-dependencies
+ process-test-resources
+
+ unpack-dependencies
+
+
+ che-core-sql-schema, che-multiuser-sql-schema
+ che-schema/
+ ${project.build.directory}
+
+
+
+
+
+
+
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/AbstractPermissionsDomain.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/AbstractPermissionsDomain.java
new file mode 100644
index 00000000000..dfd0dd7f98b
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/AbstractPermissionsDomain.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server;
+
+import com.google.common.collect.ImmutableList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import org.eclipse.che.multiuser.api.permission.server.model.impl.AbstractPermissions;
+import org.eclipse.che.multiuser.api.permission.shared.model.PermissionsDomain;
+
+/**
+ * Abstract implementation for {@link PermissionsDomain}
+ *
+ *
Note: It supports "setPermission" by default
+ *
+ * @author Sergii Leschenko
+ */
+public abstract class AbstractPermissionsDomain
+ implements PermissionsDomain {
+ public static final String SET_PERMISSIONS = "setPermissions";
+
+ private final String id;
+ private final List allowedActions;
+ private final boolean requiresInstance;
+
+ protected AbstractPermissionsDomain(String id, List allowedActions) {
+ this(id, allowedActions, true);
+ }
+
+ protected AbstractPermissionsDomain(
+ String id, List allowedActions, boolean requiresInstance) {
+ this.id = id;
+ Set resultActions = new HashSet<>(allowedActions);
+ resultActions.add(SET_PERMISSIONS);
+ this.allowedActions = ImmutableList.copyOf(resultActions);
+ this.requiresInstance = requiresInstance;
+ }
+
+ @Override
+ public String getId() {
+ return id;
+ }
+
+ @Override
+ public List getAllowedActions() {
+ return allowedActions;
+ }
+
+ @Override
+ public Boolean isInstanceRequired() {
+ return requiresInstance;
+ }
+
+ /**
+ * Creates new instance of the entity related to this domain.
+ *
+ * @return new entity instance related to this domain
+ * @throws IllegalArgumentException when instance id is null when it's required
+ */
+ public T newInstance(String userId, String instanceId, List allowedActions) {
+ if (isInstanceRequired() && instanceId == null) {
+ throw new IllegalArgumentException("Given domain requires non nullable value for instanceId");
+ }
+ return doCreateInstance(userId, instanceId, allowedActions);
+ }
+
+ protected abstract T doCreateInstance(
+ String userId, String instanceId, List allowedActions);
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof AbstractPermissionsDomain)) return false;
+ final AbstractPermissionsDomain other = (AbstractPermissionsDomain) obj;
+ return Objects.equals(id, other.id) && Objects.equals(allowedActions, other.allowedActions);
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 31 * hash + Objects.hashCode(id);
+ hash = 31 * hash + Objects.hashCode(allowedActions);
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ return "PermissionsDomain{" + "id='" + id + '\'' + ", allowedActions=" + allowedActions + "}";
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/InstanceParameterValidator.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/InstanceParameterValidator.java
new file mode 100644
index 00000000000..0d6afae1016
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/InstanceParameterValidator.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.commons.annotation.Nullable;
+
+/**
+ * Validates that provided instance parameter is valid
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class InstanceParameterValidator {
+ private final PermissionsManager permissionsManager;
+
+ @Inject
+ public InstanceParameterValidator(PermissionsManager permissionsManager) {
+ this.permissionsManager = permissionsManager;
+ }
+
+ /**
+ * Validates that provided instance parameter is valid for specified domain
+ *
+ * @param domain the domain of specified {@code instance}
+ * @param instance the instance to check
+ * @throws BadRequestException if specified {@code domain} is null
+ * @throws BadRequestException if specified {@code instance} is not valid
+ * @throws NotFoundException if specified {@code domain} is unsupported
+ */
+ public void validate(String domain, @Nullable String instance)
+ throws BadRequestException, NotFoundException {
+ checkArgument(domain != null, "Domain id required");
+ if (permissionsManager.getDomain(domain).isInstanceRequired() && instance == null) {
+ throw new BadRequestException("Specified domain requires non nullable value for instance");
+ }
+ }
+
+ private void checkArgument(boolean expression, String message) throws BadRequestException {
+ if (!expression) {
+ throw new BadRequestException(message);
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionsManager.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionsManager.java
new file mode 100644
index 00000000000..a12df9f97c1
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionsManager.java
@@ -0,0 +1,268 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server;
+
+import static com.google.common.base.MoreObjects.firstNonNull;
+import static org.eclipse.che.multiuser.api.permission.server.AbstractPermissionsDomain.SET_PERMISSIONS;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.Pages;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.notification.EventService;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.lang.concurrent.StripedLocks;
+import org.eclipse.che.commons.lang.concurrent.Unlocker;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.multiuser.api.permission.server.event.PermissionsCreatedEvent;
+import org.eclipse.che.multiuser.api.permission.server.event.PermissionsRemovedEvent;
+import org.eclipse.che.multiuser.api.permission.server.model.impl.AbstractPermissions;
+import org.eclipse.che.multiuser.api.permission.server.spi.PermissionsDao;
+import org.eclipse.che.multiuser.api.permission.shared.model.Permissions;
+
+/**
+ * Facade for Permissions related operations.
+ *
+ * @author gazarenkov
+ * @author Sergii Leschenko
+ * @author Anton Korneta
+ */
+@Singleton
+public class PermissionsManager {
+
+ private final EventService eventService;
+
+ private final List> domains;
+ private final Map> domainToDao;
+ private final StripedLocks updateLocks;
+
+ @Inject
+ public PermissionsManager(EventService eventService) throws ServerException {
+ this.eventService = eventService;
+ final Map> domainToDao = new HashMap<>();
+ final List> domains =
+ new ArrayList<>();
+ this.domains = ImmutableList.copyOf(domains);
+ this.domainToDao = ImmutableMap.copyOf(domainToDao);
+ this.updateLocks = new StripedLocks(16);
+ }
+
+ /**
+ * Stores (adds or updates) permissions.
+ *
+ * @param permissions permission to store
+ * @throws NotFoundException when permissions have unsupported domain
+ * @throws ConflictException when new permissions remove last 'setPermissions' of given instance
+ * @throws ServerException when any other error occurs during permissions storing
+ */
+ public void storePermission(Permissions permissions)
+ throws ServerException, ConflictException, NotFoundException {
+ final String domainId = permissions.getDomainId();
+ final String instanceId = permissions.getInstanceId();
+ final String userId = permissions.getUserId();
+
+ try (@SuppressWarnings("unused")
+ Unlocker unlocker = updateLocks.writeLock(firstNonNull(instanceId, domainId))) {
+ final PermissionsDao extends AbstractPermissions> permissionsDao =
+ getPermissionsDao(domainId);
+ if (!permissions.getActions().contains(SET_PERMISSIONS)
+ && userHasLastSetPermissions(permissionsDao, userId, instanceId)) {
+ throw new ConflictException(
+ "Can't edit permissions because there is not any another user "
+ + "with permission 'setPermissions'");
+ }
+ store(permissionsDao, userId, instanceId, permissions);
+ }
+ }
+
+ /**
+ * Returns user's permissions for specified instance
+ *
+ * @param userId user id
+ * @param domainId domain id
+ * @param instanceId instance id
+ * @return userId's permissions for specified instanceId
+ * @throws NotFoundException when given domainId is unsupported
+ * @throws NotFoundException when permissions with given userId and domainId and instanceId was
+ * not found
+ * @throws ServerException when any other error occurs during permissions fetching
+ */
+ public AbstractPermissions get(String userId, String domainId, String instanceId)
+ throws ServerException, NotFoundException, ConflictException {
+ return getPermissionsDao(domainId).get(userId, instanceId);
+ }
+
+ /**
+ * Returns users' permissions for specified instance
+ *
+ * @param domainId domain id
+ * @param instanceId instance id
+ * @param maxItems the maximum number of permissions to return
+ * @param skipCount the number of permissions to skip
+ * @return set of permissions
+ * @throws NotFoundException when given domainId is unsupported
+ * @throws ServerException when any other error occurs during permissions fetching
+ */
+ @SuppressWarnings("unchecked")
+ public Page getByInstance(
+ String domainId, String instanceId, int maxItems, long skipCount)
+ throws ServerException, NotFoundException {
+ return (Page)
+ getPermissionsDao(domainId).getByInstance(instanceId, maxItems, skipCount);
+ }
+
+ /**
+ * Removes permissions of userId related to the particular instanceId of specified domainId
+ *
+ * @param userId user id
+ * @param domainId domain id
+ * @param instanceId instance id
+ * @throws NotFoundException when given domainId is unsupported
+ * @throws ConflictException when removes last 'setPermissions' of given instanceId
+ * @throws ServerException when any other error occurs during permissions removing
+ */
+ public void remove(String userId, String domainId, String instanceId)
+ throws ConflictException, ServerException, NotFoundException {
+ final PermissionsDao extends AbstractPermissions> permissionsDao =
+ getPermissionsDao(domainId);
+ Permissions permissions;
+ try (@SuppressWarnings("unused")
+ Unlocker unlocker = updateLocks.writeLock(firstNonNull(instanceId, domainId))) {
+ if (userHasLastSetPermissions(permissionsDao, userId, instanceId)) {
+ throw new ConflictException(
+ "Can't remove permissions because there is not any another user "
+ + "with permission 'setPermissions'");
+ }
+ permissions = permissionsDao.get(userId, instanceId);
+ permissionsDao.remove(userId, instanceId);
+ }
+ final String initiator = EnvironmentContext.getCurrent().getSubject().getUserName();
+ eventService.publish(new PermissionsRemovedEvent(initiator, permissions));
+ }
+
+ /**
+ * Checks existence of user's permission for specified instance
+ *
+ * @param userId user id
+ * @param domainId domain id
+ * @param instanceId instance id
+ * @param action action name
+ * @return true if the permission exists
+ * @throws NotFoundException when given domain is unsupported
+ * @throws ServerException when any other error occurs during permission existence checking
+ */
+ public boolean exists(String userId, String domainId, String instanceId, String action)
+ throws ServerException, NotFoundException, ConflictException {
+ return getDomain(domainId).getAllowedActions().contains(action)
+ && getPermissionsDao(domainId).exists(userId, instanceId, action);
+ }
+
+ /**
+ * Checks supporting all specified actions by domain with specified id.
+ *
+ * @param domainId domain id to check supporting
+ * @param actions actions to check
+ * @throws NotFoundException when domain with specified id is unsupported
+ * @throws ConflictException when actions contain unsupported value
+ */
+ public void checkActionsSupporting(String domainId, List actions)
+ throws NotFoundException, ConflictException {
+ checkActionsSupporting(getDomain(domainId), actions);
+ }
+
+ /** Returns supported domains */
+ public List getDomains() {
+ return new ArrayList<>(domains);
+ }
+
+ /**
+ * Returns supported domain
+ *
+ * @throws NotFoundException when given domain is unsupported
+ */
+ public AbstractPermissionsDomain extends AbstractPermissions> getDomain(String domain)
+ throws NotFoundException {
+ return getPermissionsDao(domain).getDomain();
+ }
+
+ private void store(
+ PermissionsDao dao, String userId, String instanceId, Permissions permissions)
+ throws ConflictException, ServerException {
+ final AbstractPermissionsDomain permissionsDomain = dao.getDomain();
+ final T permission =
+ permissionsDomain.newInstance(userId, instanceId, permissions.getActions());
+ checkActionsSupporting(permissionsDomain, permission.getActions());
+ final Optional existing = dao.store(permission);
+ if (!existing.isPresent()) {
+ Subject subject = EnvironmentContext.getCurrent().getSubject();
+ final String initiator = subject.isAnonymous() ? null : subject.getUserName();
+ eventService.publish(new PermissionsCreatedEvent(initiator, permissions));
+ }
+ }
+
+ private void checkActionsSupporting(AbstractPermissionsDomain> domain, List actions)
+ throws ConflictException {
+ final Set allowedActions = new HashSet<>(domain.getAllowedActions());
+ final Set unsupportedActions =
+ actions.stream()
+ .filter(action -> !allowedActions.contains(action))
+ .collect(Collectors.toSet());
+ if (!unsupportedActions.isEmpty()) {
+ throw new ConflictException(
+ "Domain with id '"
+ + domain.getId()
+ + "' doesn't support following action(s): "
+ + unsupportedActions.stream().collect(Collectors.joining(", ")));
+ }
+ }
+
+ private PermissionsDao extends AbstractPermissions> getPermissionsDao(String domain)
+ throws NotFoundException {
+ final PermissionsDao extends AbstractPermissions> permissionsStorage =
+ domainToDao.get(domain);
+ if (permissionsStorage == null) {
+ throw new NotFoundException("Requested unsupported domain '" + domain + "'");
+ }
+ return permissionsStorage;
+ }
+
+ private boolean userHasLastSetPermissions(
+ PermissionsDao extends AbstractPermissions> storage, String userId, String instanceId)
+ throws ServerException, ConflictException, NotFoundException {
+ if (!storage.exists(userId, instanceId, SET_PERMISSIONS)) {
+ return false;
+ }
+
+ for (AbstractPermissions permissions :
+ Pages.iterateLazily(
+ (maxItems, skipCount) -> storage.getByInstance(instanceId, maxItems, skipCount))) {
+ if (!permissions.getUserId().equals(userId)
+ && permissions.getActions().contains(SET_PERMISSIONS)) {
+ return false;
+ }
+ }
+ return true;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionsModule.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionsModule.java
new file mode 100644
index 00000000000..4934735b0f1
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionsModule.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.MapBinder;
+import com.google.inject.multibindings.Multibinder;
+import com.google.inject.name.Names;
+import org.eclipse.che.multiuser.api.permission.server.account.AccountPermissionsChecker;
+import org.eclipse.che.multiuser.api.permission.server.filter.GetPermissionsFilter;
+import org.eclipse.che.multiuser.api.permission.server.filter.RemovePermissionsFilter;
+import org.eclipse.che.multiuser.api.permission.server.filter.SetPermissionsFilter;
+import org.eclipse.che.multiuser.api.permission.server.filter.check.RemovePermissionsChecker;
+import org.eclipse.che.multiuser.api.permission.server.filter.check.SetPermissionsChecker;
+import org.eclipse.che.multiuser.api.permission.server.jsonrpc.RemoteSubscriptionPermissionManager;
+
+/** @author Sergii Leschenko */
+public class PermissionsModule extends AbstractModule {
+ @Override
+ protected void configure() {
+
+ bind(SetPermissionsFilter.class);
+ bind(RemovePermissionsFilter.class);
+ bind(GetPermissionsFilter.class);
+ bind(RemoteSubscriptionPermissionManager.class).asEagerSingleton();
+
+ // Creates empty multibinder to avoid error during container starting
+ Multibinder.newSetBinder(
+ binder(), String.class, Names.named(SystemDomain.SYSTEM_DOMAIN_ACTIONS));
+
+ // initialize empty set binder
+ Multibinder.newSetBinder(binder(), AccountPermissionsChecker.class);
+ MapBinder.newMapBinder(binder(), String.class, SetPermissionsChecker.class);
+ MapBinder.newMapBinder(binder(), String.class, RemovePermissionsChecker.class);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionsService.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionsService.java
new file mode 100644
index 00000000000..692e6ac737a
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/PermissionsService.java
@@ -0,0 +1,264 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
+import static java.util.Collections.singletonList;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.Response;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.rest.Service;
+import org.eclipse.che.api.core.rest.annotations.Required;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.api.permission.server.model.impl.AbstractPermissions;
+import org.eclipse.che.multiuser.api.permission.shared.dto.DomainDto;
+import org.eclipse.che.multiuser.api.permission.shared.dto.PermissionsDto;
+import org.eclipse.che.multiuser.api.permission.shared.model.Permissions;
+import org.eclipse.che.multiuser.api.permission.shared.model.PermissionsDomain;
+
+/**
+ * Defines Permissions REST API
+ *
+ * @author Sergii Leschenko
+ */
+@Deprecated
+@Tag(name = "permissions", description = "Permissions REST API")
+@Path("/permissions")
+public class PermissionsService extends Service {
+ private final PermissionsManager permissionsManager;
+ private final InstanceParameterValidator instanceValidator;
+
+ @Inject
+ public PermissionsService(
+ PermissionsManager permissionsManager, InstanceParameterValidator instanceValidator) {
+ this.permissionsManager = permissionsManager;
+ this.instanceValidator = instanceValidator;
+ }
+
+ @GET
+ @Produces(APPLICATION_JSON)
+ @Operation(
+ summary = "Get all supported domains or only requested if domain parameter specified",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The domains successfully fetched",
+ content =
+ @Content(array = @ArraySchema(schema = @Schema(implementation = DomainDto.class)))),
+ @ApiResponse(responseCode = "404", description = "Requested domain is not supported"),
+ @ApiResponse(
+ responseCode = "500",
+ description = "Internal server error occurred during domains fetching")
+ })
+ public List getSupportedDomains(
+ @Parameter(description = "Id of requested domain") @QueryParam("domain") String domainId)
+ throws NotFoundException {
+ if (isNullOrEmpty(domainId)) {
+ return permissionsManager.getDomains().stream().map(this::asDto).collect(Collectors.toList());
+ } else {
+ return singletonList(asDto(permissionsManager.getDomain(domainId)));
+ }
+ }
+
+ @POST
+ @Consumes(APPLICATION_JSON)
+ @Operation(
+ summary = "Store given permissions",
+ responses = {
+ @ApiResponse(responseCode = "200", description = "The permissions successfully stored"),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Missed required parameters, parameters are not valid"),
+ @ApiResponse(responseCode = "404", description = "Domain of permissions is not supported"),
+ @ApiResponse(
+ responseCode = "409",
+ description = "New permissions removes last 'setPermissions' of given instance"),
+ @ApiResponse(
+ responseCode = "409",
+ description = "Given domain requires non nullable value for instance but it is null"),
+ @ApiResponse(
+ responseCode = "500",
+ description = "Internal server error occurred during permissions storing")
+ })
+ public void storePermissions(
+ @Parameter(description = "The permissions to store", required = true)
+ PermissionsDto permissionsDto)
+ throws ServerException, BadRequestException, ConflictException, NotFoundException {
+ checkArgument(permissionsDto != null, "Permissions descriptor required");
+ checkArgument(!isNullOrEmpty(permissionsDto.getUserId()), "User required");
+ checkArgument(!isNullOrEmpty(permissionsDto.getDomainId()), "Domain required");
+ instanceValidator.validate(permissionsDto.getDomainId(), permissionsDto.getInstanceId());
+ checkArgument(!permissionsDto.getActions().isEmpty(), "One or more actions required");
+
+ permissionsManager.storePermission(permissionsDto);
+ }
+
+ @GET
+ @Path("/{domain}")
+ @Produces(APPLICATION_JSON)
+ @Operation(
+ summary =
+ "Get permissions of current user which are related to specified sdomain and instance",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The permissions successfully fetched",
+ content = @Content(schema = @Schema(implementation = PermissionsDto.class))),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Missed required parameters, parameters are not valid"),
+ @ApiResponse(responseCode = "404", description = "Specified domain is unsupported"),
+ @ApiResponse(
+ responseCode = "404",
+ description =
+ "Permissions for current user with specified domain and instance was not found"),
+ @ApiResponse(
+ responseCode = "409",
+ description = "Given domain requires non nullable value for instance but it is null"),
+ @ApiResponse(
+ responseCode = "500",
+ description = "Internal server error occurred during permissions fetching")
+ })
+ public PermissionsDto getCurrentUsersPermissions(
+ @Parameter(description = "Domain id to retrieve user's permissions") @PathParam("domain")
+ String domain,
+ @Parameter(description = "Instance id to retrieve user's permissions") @QueryParam("instance")
+ String instance)
+ throws BadRequestException, NotFoundException, ConflictException, ServerException {
+ instanceValidator.validate(domain, instance);
+ return toDto(
+ permissionsManager.get(
+ EnvironmentContext.getCurrent().getSubject().getUserId(), domain, instance));
+ }
+
+ @GET
+ @Path("/{domain}/all")
+ @Produces(APPLICATION_JSON)
+ @Operation(
+ summary = "Get permissions which are related to specified domain and instance",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The permissions successfully fetched",
+ content =
+ @Content(
+ array = @ArraySchema(schema = @Schema(implementation = PermissionsDto.class)))),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Missed required parameters, parameters are not valid"),
+ @ApiResponse(responseCode = "404", description = "Specified domain is unsupported"),
+ @ApiResponse(
+ responseCode = "409",
+ description = "Given domain requires non nullable value for instance but it is null"),
+ @ApiResponse(
+ responseCode = "500",
+ description = "Internal server error occurred during permissions fetching")
+ })
+ public Response getUsersPermissions(
+ @Parameter(description = "Domain id to retrieve users' permissions") @PathParam("domain")
+ String domain,
+ @Parameter(description = "Instance id to retrieve users' permissions") @QueryParam("instance")
+ String instance,
+ @Parameter(description = "Max items") @QueryParam("maxItems") @DefaultValue("30")
+ int maxItems,
+ @Parameter(description = "Skip count") @QueryParam("skipCount") @DefaultValue("0")
+ int skipCount)
+ throws ServerException, NotFoundException, ConflictException, BadRequestException {
+ instanceValidator.validate(domain, instance);
+ checkArgument(maxItems >= 0, "The number of items to return can't be negative.");
+ checkArgument(skipCount >= 0, "The number of items to skip can't be negative.");
+
+ final Page permissionsPage =
+ permissionsManager.getByInstance(domain, instance, maxItems, skipCount);
+ return Response.ok()
+ .entity(permissionsPage.getItems(this::toDto))
+ .header("Link", createLinkHeader(permissionsPage))
+ .build();
+ }
+
+ @DELETE
+ @Path("/{domain}")
+ @Operation(
+ summary = "Removes user's permissions related to the particular instance of specified domain",
+ responses = {
+ @ApiResponse(responseCode = "204", description = "The permissions successfully removed"),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Missed required parameters, parameters are not valid"),
+ @ApiResponse(responseCode = "404", description = "Specified domain is unsupported"),
+ @ApiResponse(
+ responseCode = "409",
+ description = "User has last 'setPermissions' of given instance"),
+ @ApiResponse(
+ responseCode = "409",
+ description = "Given domain requires non nullable value for instance but it is null"),
+ @ApiResponse(
+ responseCode = "500",
+ description = "Internal server error occurred during permissions removing")
+ })
+ public void removePermissions(
+ @Parameter(description = "Domain id to remove user's permissions") @PathParam("domain")
+ String domain,
+ @Parameter(description = "Instance id to remove user's permissions") @QueryParam("instance")
+ String instance,
+ @Parameter(description = "User id", required = true) @QueryParam("user") @Required
+ String user)
+ throws BadRequestException, NotFoundException, ConflictException, ServerException {
+ instanceValidator.validate(domain, instance);
+ permissionsManager.remove(user, domain, instance);
+ }
+
+ private DomainDto asDto(PermissionsDomain domain) {
+ return DtoFactory.newDto(DomainDto.class)
+ .withId(domain.getId())
+ .withAllowedActions(domain.getAllowedActions());
+ }
+
+ private void checkArgument(boolean expression, String message) throws BadRequestException {
+ if (!expression) {
+ throw new BadRequestException(message);
+ }
+ }
+
+ private PermissionsDto toDto(Permissions permissions) {
+ return DtoFactory.newDto(PermissionsDto.class)
+ .withUserId(permissions.getUserId())
+ .withDomainId(permissions.getDomainId())
+ .withInstanceId(permissions.getInstanceId())
+ .withActions(permissions.getActions());
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/SuperPrivilegesChecker.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/SuperPrivilegesChecker.java
new file mode 100644
index 00000000000..91dd97fab52
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/SuperPrivilegesChecker.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server;
+
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Named;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.multiuser.api.permission.shared.model.PermissionsDomain;
+
+/**
+ * Checks that current subject has privileges to perform some operation without required
+ * permissions.
+ *
+ *
Super privileges is designed to give some extra abilities for users who have permission to
+ * perform {@link SystemDomain#MANAGE_SYSTEM_ACTION manage system}.
+ * Super privileges are optional, they can be disabled by configuration.
+ *
+ *
User has super privileges if he has {@link SystemDomain#MANAGE_SYSTEM_ACTION manage system}
+ * permission and system configuration property {@link #SYSTEM_SUPER_PRIVILEGED_MODE} is true.
+ *
+ *
It is required to perform {@link #hasSuperPrivileges()} checks manually before permissions
+ * checking if user should be able to perform some operation.
+ *
+ *
+ *
+ * If user should be able to manage permissions for some permission domain then this domain should
+ * be present in multibinder named with {@link #SUPER_PRIVILEGED_DOMAINS}.
+ * Binding example:
+ *
+ *
+ *
+ * @author Sergii Leschenko
+ */
+public class SuperPrivilegesChecker {
+ /**
+ * Configuration parameter that indicates extended abilities for users who have {@link
+ * SystemDomain#MANAGE_SYSTEM_ACTION manageSytem} permission.
+ */
+ public static final String SYSTEM_SUPER_PRIVILEGED_MODE = "che.system.super_privileged_mode";
+
+ /** Permissions of these domains can be managed by any user who has super privileges. */
+ public static final String SUPER_PRIVILEGED_DOMAINS = "system.super_privileged_domains";
+
+ private final boolean superPrivilegedMode;
+ private final Set privilegesDomainsIds;
+
+ @Inject
+ public SuperPrivilegesChecker(
+ @Named(SYSTEM_SUPER_PRIVILEGED_MODE) boolean superPrivilegedMode,
+ @Named(SUPER_PRIVILEGED_DOMAINS) Set domains) {
+ this.superPrivilegedMode = superPrivilegedMode;
+ this.privilegesDomainsIds =
+ domains.stream().map(PermissionsDomain::getId).collect(Collectors.toSet());
+ }
+
+ /**
+ * Checks that current subject has super privileges.
+ *
+ * @return true if current subject has super privileges, false otherwise
+ */
+ public boolean hasSuperPrivileges() {
+ return superPrivilegedMode
+ && EnvironmentContext.getCurrent()
+ .getSubject()
+ .hasPermission(SystemDomain.DOMAIN_ID, null, SystemDomain.MANAGE_SYSTEM_ACTION);
+ }
+
+ /**
+ * Checks that current subject is privileged to manage permissions of specified domain.
+ *
+ * @return true if current subject is privileged to manage permissions of specified domain, false
+ * otherwise
+ */
+ public boolean isPrivilegedToManagePermissions(String domainId) {
+ return superPrivilegedMode
+ && privilegesDomainsIds.contains(domainId)
+ && EnvironmentContext.getCurrent()
+ .getSubject()
+ .hasPermission(SystemDomain.DOMAIN_ID, null, SystemDomain.MANAGE_SYSTEM_ACTION);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/SystemDomain.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/SystemDomain.java
new file mode 100644
index 00000000000..cb9e27d879e
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/SystemDomain.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server;
+
+import static java.util.stream.Collectors.toList;
+
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Stream;
+import javax.inject.Inject;
+import javax.inject.Named;
+import org.eclipse.che.multiuser.api.permission.server.model.impl.SystemPermissionsImpl;
+
+/**
+ * Domain for storing actions that are used for managing system e.g. user management, configuration properties management.
+ *
+ *
The list of supported actions by system domain can be configured by following lines
+ *
+ * Multibinder binder = Multibinder.newSetBinder(binder(), String.class, Names.named(SystemDomain.SYSTEM_DOMAIN_ACTIONS));
+ * binder.addBinding().toInstance("customAction");
+ *
+ *
+ * @author Sergii Leschenko
+ */
+public class SystemDomain extends AbstractPermissionsDomain {
+ public static final String SYSTEM_DOMAIN_ACTIONS = "system.domain.actions";
+ public static final String DOMAIN_ID = "system";
+ public static final String MANAGE_SYSTEM_ACTION = "manageSystem";
+ public static final String MONITOR_SYSTEM_ACTION = "monitorSystem";
+
+ @Inject
+ public SystemDomain(@Named(SYSTEM_DOMAIN_ACTIONS) Set allowedActions) {
+ super(
+ DOMAIN_ID,
+ Stream.concat(
+ allowedActions.stream(), Stream.of(MANAGE_SYSTEM_ACTION, MONITOR_SYSTEM_ACTION))
+ .collect(toList()),
+ false);
+ }
+
+ @Override
+ public SystemPermissionsImpl doCreateInstance(
+ String userId, String instanceId, List allowedActions) {
+ return new SystemPermissionsImpl(userId, allowedActions);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/account/AccountOperation.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/account/AccountOperation.java
new file mode 100644
index 00000000000..c7435e461f4
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/account/AccountOperation.java
@@ -0,0 +1,28 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.account;
+
+/**
+ * Actions that can be performed by users in accounts.
+ *
+ * @author Sergii Leshchenko
+ */
+public enum AccountOperation {
+ /** When user creates workspace that will belong to account. */
+ CREATE_WORKSPACE,
+
+ /** When user does any operation with existing workspace. */
+ MANAGE_WORKSPACES,
+
+ /** When user retrieves information about account resources(like available, total, etc). */
+ SEE_RESOURCE_INFORMATION
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/account/AccountPermissionsChecker.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/account/AccountPermissionsChecker.java
new file mode 100644
index 00000000000..493fb6242e3
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/account/AccountPermissionsChecker.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.account;
+
+import org.eclipse.che.api.core.ForbiddenException;
+
+/**
+ * Defines permissions checking for accounts with some type.
+ *
+ * @author Sergii Leshchenko
+ */
+public interface AccountPermissionsChecker {
+ /**
+ * Checks that current subject is authorized to perform given operation with specified account
+ *
+ * @param accountId account to check
+ * @param operation operation that is going to be performed
+ * @throws ForbiddenException when user doesn't have permissions to perform specified operation
+ */
+ void checkPermissions(String accountId, AccountOperation operation) throws ForbiddenException;
+
+ /** Returns account type for which this class tracks check resources permissions. */
+ String getAccountType();
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/event/PermissionsCreatedEvent.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/event/PermissionsCreatedEvent.java
new file mode 100644
index 00000000000..14261323b79
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/event/PermissionsCreatedEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.event;
+
+import static org.eclipse.che.multiuser.api.permission.shared.event.EventType.PERMISSIONS_ADDED;
+
+import org.eclipse.che.commons.annotation.Nullable;
+import org.eclipse.che.multiuser.api.permission.shared.event.EventType;
+import org.eclipse.che.multiuser.api.permission.shared.event.PermissionsEvent;
+import org.eclipse.che.multiuser.api.permission.shared.model.Permissions;
+
+/**
+ * Defines permissions added events.
+ *
+ * @author Anton Korneta
+ */
+public class PermissionsCreatedEvent implements PermissionsEvent {
+
+ private final String initiator;
+ private final Permissions permissions;
+
+ public PermissionsCreatedEvent(String initiator, Permissions permissions) {
+ this.initiator = initiator;
+ this.permissions = permissions;
+ }
+
+ @Override
+ public EventType getType() {
+ return PERMISSIONS_ADDED;
+ }
+
+ @Override
+ public Permissions getPermissions() {
+ return permissions;
+ }
+
+ @Nullable
+ @Override
+ public String getInitiator() {
+ return initiator;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/event/PermissionsRemovedEvent.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/event/PermissionsRemovedEvent.java
new file mode 100644
index 00000000000..0802724be02
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/event/PermissionsRemovedEvent.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.event;
+
+import static org.eclipse.che.multiuser.api.permission.shared.event.EventType.PERMISSIONS_REMOVED;
+
+import org.eclipse.che.commons.annotation.Nullable;
+import org.eclipse.che.multiuser.api.permission.shared.event.EventType;
+import org.eclipse.che.multiuser.api.permission.shared.event.PermissionsEvent;
+import org.eclipse.che.multiuser.api.permission.shared.model.Permissions;
+
+/**
+ * Defines permissions added events.
+ *
+ * @author Anton Korneta
+ */
+public class PermissionsRemovedEvent implements PermissionsEvent {
+
+ private final String initiator;
+ private final Permissions permissions;
+
+ public PermissionsRemovedEvent(String initiator, Permissions permissions) {
+ this.initiator = initiator;
+ this.permissions = permissions;
+ }
+
+ @Override
+ public EventType getType() {
+ return PERMISSIONS_REMOVED;
+ }
+
+ @Override
+ public Permissions getPermissions() {
+ return permissions;
+ }
+
+ @Nullable
+ @Override
+ public String getInitiator() {
+ return initiator;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/GetPermissionsFilter.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/GetPermissionsFilter.java
new file mode 100644
index 00000000000..765ba0668ec
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/GetPermissionsFilter.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.filter;
+
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.QueryParam;
+import javax.inject.Inject;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.everrest.CheMethodInvokerFilter;
+import org.eclipse.che.multiuser.api.permission.server.InstanceParameterValidator;
+import org.eclipse.che.multiuser.api.permission.server.PermissionsManager;
+import org.eclipse.che.multiuser.api.permission.server.SuperPrivilegesChecker;
+import org.everrest.core.Filter;
+import org.everrest.core.resource.GenericResourceMethod;
+
+/**
+ * Restricts access to reading permissions of instance by users' readPermissions permission
+ *
+ * @author Sergii Leschenko
+ */
+@Filter
+@Path("/permissions/{domain}/all")
+public class GetPermissionsFilter extends CheMethodInvokerFilter {
+ @PathParam("domain")
+ private String domain;
+
+ @QueryParam("instance")
+ private String instance;
+
+ @Inject private PermissionsManager permissionsManager;
+
+ @Inject private SuperPrivilegesChecker superPrivilegesChecker;
+
+ @Inject private InstanceParameterValidator instanceValidator;
+
+ @Override
+ public void filter(GenericResourceMethod genericResourceMethod, Object[] arguments)
+ throws BadRequestException, NotFoundException, ConflictException, ForbiddenException,
+ ServerException {
+
+ final String methodName = genericResourceMethod.getMethod().getName();
+ if (methodName.equals("getUsersPermissions")) {
+ instanceValidator.validate(domain, instance);
+ if (superPrivilegesChecker.isPrivilegedToManagePermissions(domain)) {
+ return;
+ }
+ final String userId = EnvironmentContext.getCurrent().getSubject().getUserId();
+ try {
+ permissionsManager.get(userId, domain, instance);
+ // user should have ability to see another users' permissions if he has any permission there
+ } catch (NotFoundException e) {
+ throw new ForbiddenException("User is not authorized to perform this operation");
+ }
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/RemovePermissionsFilter.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/RemovePermissionsFilter.java
new file mode 100644
index 00000000000..23b34b40318
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/RemovePermissionsFilter.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.filter;
+
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.QueryParam;
+import javax.inject.Inject;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.everrest.CheMethodInvokerFilter;
+import org.eclipse.che.multiuser.api.permission.server.InstanceParameterValidator;
+import org.eclipse.che.multiuser.api.permission.server.SuperPrivilegesChecker;
+import org.eclipse.che.multiuser.api.permission.server.filter.check.DomainsPermissionsCheckers;
+import org.everrest.core.Filter;
+import org.everrest.core.resource.GenericResourceMethod;
+
+/**
+ * Restricts access to removing permissions of instance by users' setPermissions permission
+ *
+ * @author Sergii Leschenko
+ */
+@Filter
+@Path("/permissions/{domain}")
+public class RemovePermissionsFilter extends CheMethodInvokerFilter {
+ @PathParam("domain")
+ private String domain;
+
+ @QueryParam("instance")
+ private String instance;
+
+ @QueryParam("user")
+ private String user;
+
+ @Inject private SuperPrivilegesChecker superPrivilegesChecker;
+
+ @Inject private InstanceParameterValidator instanceValidator;
+
+ @Inject private DomainsPermissionsCheckers domainsPermissionsCheckers;
+
+ @Override
+ public void filter(GenericResourceMethod genericResourceMethod, Object[] args)
+ throws BadRequestException, ForbiddenException, NotFoundException, ServerException {
+ if (genericResourceMethod.getMethod().getName().equals("removePermissions")) {
+ instanceValidator.validate(domain, instance);
+ final Subject currentSubject = EnvironmentContext.getCurrent().getSubject();
+ if (currentSubject.getUserId().equals(user)
+ || superPrivilegesChecker.isPrivilegedToManagePermissions(domain)) {
+ return;
+ }
+ domainsPermissionsCheckers.getRemoveChecker(domain).check(user, domain, instance);
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/SetPermissionsFilter.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/SetPermissionsFilter.java
new file mode 100644
index 00000000000..e39a174d1d2
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/SetPermissionsFilter.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.filter;
+
+import static com.google.common.base.Strings.isNullOrEmpty;
+
+import jakarta.ws.rs.Path;
+import javax.inject.Inject;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.everrest.CheMethodInvokerFilter;
+import org.eclipse.che.multiuser.api.permission.server.InstanceParameterValidator;
+import org.eclipse.che.multiuser.api.permission.server.SuperPrivilegesChecker;
+import org.eclipse.che.multiuser.api.permission.server.filter.check.DomainsPermissionsCheckers;
+import org.eclipse.che.multiuser.api.permission.shared.dto.PermissionsDto;
+import org.everrest.core.Filter;
+import org.everrest.core.resource.GenericResourceMethod;
+
+/**
+ * Restricts access to setting permissions of instance by users' setPermissions permission
+ *
+ * @author Sergii Leschenko
+ */
+@Filter
+@Path("/permissions/")
+public class SetPermissionsFilter extends CheMethodInvokerFilter {
+ @Inject private SuperPrivilegesChecker superPrivilegesChecker;
+
+ @Inject private InstanceParameterValidator instanceValidator;
+
+ @Inject private DomainsPermissionsCheckers domainsPermissionsChecker;
+
+ @Override
+ public void filter(GenericResourceMethod genericResourceMethod, Object[] args)
+ throws BadRequestException, ForbiddenException, NotFoundException, ServerException {
+ if (genericResourceMethod.getMethod().getName().equals("storePermissions")) {
+ final PermissionsDto permissions = (PermissionsDto) args[0];
+ checkArgument(permissions != null, "Permissions descriptor required");
+ final String domain = permissions.getDomainId();
+ checkArgument(!isNullOrEmpty(domain), "Domain required");
+ instanceValidator.validate(domain, permissions.getInstanceId());
+ if (superPrivilegesChecker.isPrivilegedToManagePermissions(permissions.getDomainId())) {
+ return;
+ }
+ domainsPermissionsChecker.getSetChecker(domain).check(permissions);
+ }
+ }
+
+ private void checkArgument(boolean expression, String message) throws BadRequestException {
+ if (!expression) {
+ throw new BadRequestException(message);
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/DefaultRemovePermissionsChecker.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/DefaultRemovePermissionsChecker.java
new file mode 100644
index 00000000000..6cf32871dd0
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/DefaultRemovePermissionsChecker.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.filter.check;
+
+import static org.eclipse.che.multiuser.api.permission.server.AbstractPermissionsDomain.SET_PERMISSIONS;
+
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.commons.env.EnvironmentContext;
+
+/**
+ * Common checks while removing permissions.
+ *
+ * @author Anton Korneta
+ */
+@Singleton
+public class DefaultRemovePermissionsChecker implements RemovePermissionsChecker {
+
+ @Override
+ public void check(String user, String domainId, String instance) throws ForbiddenException {
+ if (!EnvironmentContext.getCurrent()
+ .getSubject()
+ .hasPermission(domainId, instance, SET_PERMISSIONS)) {
+ throw new ForbiddenException("User can't edit permissions for this instance");
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/DefaultSetPermissionsChecker.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/DefaultSetPermissionsChecker.java
new file mode 100644
index 00000000000..9568ca85d81
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/DefaultSetPermissionsChecker.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.filter.check;
+
+import static org.eclipse.che.multiuser.api.permission.server.AbstractPermissionsDomain.SET_PERMISSIONS;
+
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.multiuser.api.permission.shared.model.Permissions;
+
+/**
+ * Common checks while setting permissions.
+ *
+ * @author Anton Korneta
+ */
+@Singleton
+public class DefaultSetPermissionsChecker implements SetPermissionsChecker {
+
+ @Override
+ public void check(Permissions permissions) throws ForbiddenException {
+ if (!EnvironmentContext.getCurrent()
+ .getSubject()
+ .hasPermission(permissions.getDomainId(), permissions.getInstanceId(), SET_PERMISSIONS)) {
+ throw new ForbiddenException("User can't edit permissions for this instance");
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/DomainsPermissionsCheckers.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/DomainsPermissionsCheckers.java
new file mode 100644
index 00000000000..e53bda19170
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/DomainsPermissionsCheckers.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.filter.check;
+
+import java.util.Map;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Represents a set of domain-specific permissions checkers.
+ *
+ * @author Anton Korneta
+ */
+@Singleton
+public class DomainsPermissionsCheckers {
+
+ private final Map domain2setPermissionsChecker;
+ private final DefaultSetPermissionsChecker defaultSetPermissionsChecker;
+ private final Map domain2removePermissionsChecker;
+ private final DefaultRemovePermissionsChecker defaultRemovePermissionsChecker;
+
+ @Inject
+ public DomainsPermissionsCheckers(
+ Map domain2setPermissionsChecker,
+ DefaultSetPermissionsChecker defaultPermissionsChecker,
+ Map domain2removePermissionsChecker,
+ DefaultRemovePermissionsChecker defaultRemovePermissionsChecker) {
+ this.domain2setPermissionsChecker = domain2setPermissionsChecker;
+ this.defaultSetPermissionsChecker = defaultPermissionsChecker;
+ this.domain2removePermissionsChecker = domain2removePermissionsChecker;
+ this.defaultRemovePermissionsChecker = defaultRemovePermissionsChecker;
+ }
+
+ public SetPermissionsChecker getSetChecker(String domain) {
+ if (domain2setPermissionsChecker.containsKey(domain)) {
+ return domain2setPermissionsChecker.get(domain);
+ }
+ return defaultSetPermissionsChecker;
+ }
+
+ public RemovePermissionsChecker getRemoveChecker(String domain) {
+ if (domain2removePermissionsChecker.containsKey(domain)) {
+ return domain2removePermissionsChecker.get(domain);
+ }
+ return defaultRemovePermissionsChecker;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/RemovePermissionsChecker.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/RemovePermissionsChecker.java
new file mode 100644
index 00000000000..64f4a581ba6
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/RemovePermissionsChecker.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.filter.check;
+
+import org.eclipse.che.api.core.ForbiddenException;
+
+/**
+ * Defines contract for domain specific checks, before remove permissions.
+ *
+ * @author Anton Korneta
+ */
+public interface RemovePermissionsChecker {
+
+ /**
+ * Checks if the current user is allowed to remove permissions.
+ *
+ * @param user user identifier
+ * @param domainId permissions domain
+ * @param instance instance associated with the permissions to be removed
+ * @throws ForbiddenException when it is not allowed to remove permissions
+ */
+ void check(String user, String domainId, String instance) throws ForbiddenException;
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/SetPermissionsChecker.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/SetPermissionsChecker.java
new file mode 100644
index 00000000000..87b3fd46f11
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/filter/check/SetPermissionsChecker.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.filter.check;
+
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.multiuser.api.permission.shared.model.Permissions;
+
+/**
+ * Defines contract for domain specific checks, before set permissions.
+ *
+ * @author Anton Korneta
+ */
+public interface SetPermissionsChecker {
+
+ /**
+ * Checks if the current user is allowed to set permissions.
+ *
+ * @param permissions permission to set
+ * @throws ForbiddenException when it is not allowed to set {@code permissions}
+ */
+ void check(Permissions permissions) throws ForbiddenException;
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/jsonrpc/JsonRpcPermissionsFilterAdapter.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/jsonrpc/JsonRpcPermissionsFilterAdapter.java
new file mode 100644
index 00000000000..c538fb77d06
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/jsonrpc/JsonRpcPermissionsFilterAdapter.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.jsonrpc;
+
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.jsonrpc.commons.JsonRpcException;
+import org.eclipse.che.api.core.jsonrpc.commons.JsonRpcMethodInvokerFilter;
+
+/**
+ * Purpose: Provides an implementation of JsonRpcMethodInvokerFilter that allow to throw
+ * {@link ForbiddenException} that will be wrapped into {@link JsonRpcException}.
+ *
+ * @author Sergii Leshchenko
+ */
+public abstract class JsonRpcPermissionsFilterAdapter implements JsonRpcMethodInvokerFilter {
+
+ @Override
+ public void accept(String method, Object... params) throws JsonRpcException {
+ try {
+ doAccept(method, params);
+ } catch (ForbiddenException e) {
+ throw new JsonRpcException(403, e.getMessage());
+ }
+ }
+
+ protected abstract void doAccept(String method, Object... params) throws ForbiddenException;
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/jsonrpc/RemoteSubscriptionPermissionCheck.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/jsonrpc/RemoteSubscriptionPermissionCheck.java
new file mode 100644
index 00000000000..2dfeb049248
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/jsonrpc/RemoteSubscriptionPermissionCheck.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.jsonrpc;
+
+import java.util.Map;
+import org.eclipse.che.api.core.ForbiddenException;
+
+/**
+ * Check that should be performed before remote subscribing.
+ *
+ * @author Sergii Leshchenko
+ * @see org.eclipse.che.api.core.notification.RemoteSubscriptionManager
+ */
+public interface RemoteSubscriptionPermissionCheck {
+
+ /**
+ * Check that the current subject is allowed to listen to the specified method events
+ *
+ * @param methodName method name to subscribe
+ * @param scope scope of subscription
+ * @throws ForbiddenException if the current subject does not have needed permissions
+ * @throws ForbiddenException if any other exception occurred during permissions check
+ */
+ void check(String methodName, Map scope) throws ForbiddenException;
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/jsonrpc/RemoteSubscriptionPermissionManager.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/jsonrpc/RemoteSubscriptionPermissionManager.java
new file mode 100644
index 00000000000..5d0f697212c
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/jsonrpc/RemoteSubscriptionPermissionManager.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.jsonrpc;
+
+import static java.lang.String.format;
+import static org.eclipse.che.api.core.notification.RemoteSubscriptionManager.SUBSCRIBE_JSON_RPC_METHOD;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.jsonrpc.commons.RequestHandlerManager;
+import org.eclipse.che.api.core.notification.dto.EventSubscription;
+
+/**
+ * Filters invocation of {@link
+ * org.eclipse.che.api.core.notification.RemoteSubscriptionManager#SUBSCRIBE_JSON_RPC_METHOD} and
+ * performs the corresponding {@link RemoteSubscriptionPermissionCheck} to make sure that user is
+ * authorized to listen to requested events.
+ *
+ * @author Sergii Leshchenko
+ */
+@Singleton
+public class RemoteSubscriptionPermissionManager {
+
+ private final Map methodToCheck;
+
+ public RemoteSubscriptionPermissionManager() {
+ this.methodToCheck = new HashMap<>();
+ }
+
+ @Inject
+ void register(RequestHandlerManager requestHandlerManager) {
+ requestHandlerManager.registerMethodInvokerFilter(
+ new RemoteSubscriptionFilter(), SUBSCRIBE_JSON_RPC_METHOD);
+ }
+
+ /**
+ * Registers permissions check for the specified methods
+ *
+ * @param permissionCheck permissions check that should be invoked before subscription
+ * @param methods methods to filter
+ * @throws IllegalStateException if any of specified methods already has registered check
+ */
+ public synchronized void registerCheck(
+ RemoteSubscriptionPermissionCheck permissionCheck, String... methods) {
+ for (String method : methods) {
+ if (methodToCheck.containsKey(method)) {
+ throw new IllegalStateException(
+ format("Permissions check is already registered for method '%s'", method));
+ }
+
+ methodToCheck.put(method, permissionCheck);
+ }
+ }
+
+ private class RemoteSubscriptionFilter extends JsonRpcPermissionsFilterAdapter {
+ @Override
+ protected void doAccept(String method, Object... params) throws ForbiddenException {
+ EventSubscription param = (EventSubscription) params[0];
+
+ RemoteSubscriptionPermissionCheck permissionCheck = methodToCheck.get(param.getMethod());
+ if (permissionCheck != null) {
+ permissionCheck.check(param.getMethod(), param.getScope());
+ }
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/model/impl/AbstractPermissions.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/model/impl/AbstractPermissions.java
new file mode 100644
index 00000000000..ad121f66e8a
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/model/impl/AbstractPermissions.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.model.impl;
+
+import java.util.List;
+import java.util.Objects;
+import javax.persistence.Column;
+import javax.persistence.GeneratedValue;
+import javax.persistence.Id;
+import javax.persistence.JoinColumn;
+import javax.persistence.MappedSuperclass;
+import javax.persistence.OneToOne;
+import javax.persistence.PostLoad;
+import javax.persistence.PrePersist;
+import javax.persistence.PreUpdate;
+import javax.persistence.Transient;
+import org.eclipse.che.api.user.server.model.impl.UserImpl;
+import org.eclipse.che.multiuser.api.permission.shared.model.Permissions;
+
+/**
+ * Represents user's permissions to access to some resources
+ *
+ * @author Sergii Leschenko
+ */
+@MappedSuperclass
+public abstract class AbstractPermissions implements Permissions {
+
+ @Id
+ @GeneratedValue
+ @Column(name = "id")
+ protected String id;
+
+ @Column(name = "user_id")
+ protected String userId;
+
+ @OneToOne
+ @JoinColumn(name = "user_id", insertable = false, updatable = false)
+ private UserImpl user;
+
+ @Transient private String userIdHolder;
+
+ public AbstractPermissions() {}
+
+ public AbstractPermissions(Permissions permissions) {
+ this(permissions.getUserId());
+ }
+
+ public AbstractPermissions(String userId) {
+ this.userIdHolder = userId;
+ this.userId = userId;
+ }
+
+ /** Returns used id */
+ @Override
+ public String getUserId() {
+ return userIdHolder;
+ }
+
+ public void setUserId(String userId) {
+ this.userIdHolder = userId;
+ }
+
+ /** Returns instance id */
+ @Override
+ public abstract String getInstanceId();
+
+ /** Returns domain id */
+ @Override
+ public abstract String getDomainId();
+
+ /** List of actions which user can perform for particular instance */
+ @Override
+ public abstract List getActions();
+
+ @PreUpdate
+ @PrePersist
+ private void prePersist() {
+ if ("*".equals(userIdHolder)) {
+ userId = null;
+ } else {
+ userId = userIdHolder;
+ }
+ }
+
+ @PostLoad
+ private void postLoad() {
+ if (userId == null) {
+ userIdHolder = "*";
+ } else {
+ userIdHolder = userId;
+ }
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) return true;
+ if (!(obj instanceof AbstractPermissions)) return false;
+ final AbstractPermissions other = (AbstractPermissions) obj;
+ return Objects.equals(id, other.id)
+ && Objects.equals(getUserId(), other.getUserId())
+ && Objects.equals(getInstanceId(), other.getInstanceId())
+ && Objects.equals(getDomainId(), other.getDomainId())
+ && Objects.equals(getActions(), other.getActions());
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 31 * hash + Objects.hashCode(id);
+ hash = 31 * hash + Objects.hashCode(getUserId());
+ hash = 31 * hash + Objects.hashCode(getInstanceId());
+ hash = 31 * hash + Objects.hashCode(getDomainId());
+ hash = 31 * hash + Objects.hashCode(getActions());
+ return hash;
+ }
+
+ @Override
+ public String toString() {
+ return "AbstractPermissions{" + "id='" + id + '\'' + ", user=" + user + '}';
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/model/impl/SystemPermissionsImpl.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/model/impl/SystemPermissionsImpl.java
new file mode 100644
index 00000000000..287254a69a5
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/model/impl/SystemPermissionsImpl.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.model.impl;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.persistence.CollectionTable;
+import javax.persistence.Column;
+import javax.persistence.ElementCollection;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.JoinColumn;
+import javax.persistence.NamedQueries;
+import javax.persistence.NamedQuery;
+import javax.persistence.Table;
+import org.eclipse.che.multiuser.api.permission.server.SystemDomain;
+
+/**
+ * System permissions data object.
+ *
+ * @author Max Shaposhnik
+ */
+@Entity(name = "SystemPermissions")
+@NamedQueries({
+ @NamedQuery(
+ name = "SystemPermissions.getByUserId",
+ query =
+ "SELECT permissions "
+ + "FROM SystemPermissions permissions "
+ + "WHERE permissions.userId = :userId "),
+ @NamedQuery(
+ name = "SystemPermissions.getAll",
+ query = "SELECT permissions " + "FROM SystemPermissions permissions "),
+ @NamedQuery(
+ name = "SystemPermissions.getTotalCount",
+ query = "SELECT COUNT(permissions) " + "FROM SystemPermissions permissions ")
+})
+@Table(name = "che_system_permissions")
+public class SystemPermissionsImpl extends AbstractPermissions {
+
+ public SystemPermissionsImpl() {}
+
+ public SystemPermissionsImpl(String userId, List actions) {
+ super(userId);
+ if (actions != null) {
+ this.actions = new ArrayList<>(actions);
+ }
+ }
+
+ public SystemPermissionsImpl(SystemPermissionsImpl permissions) {
+ this(permissions.getUserId(), permissions.getActions());
+ }
+
+ @ElementCollection(fetch = FetchType.EAGER)
+ @Column(name = "actions")
+ @CollectionTable(
+ name = "che_system_permissions_actions",
+ joinColumns = @JoinColumn(name = "system_permissions_id"))
+ protected List actions;
+
+ @Override
+ public String getInstanceId() {
+ return null;
+ }
+
+ @Override
+ public String getDomainId() {
+ return SystemDomain.DOMAIN_ID;
+ }
+
+ @Override
+ public List getActions() {
+ return actions;
+ }
+
+ @Override
+ public String toString() {
+ return "SystemPermissions{" + "user='" + getUserId() + '\'' + ", actions=" + actions + '}';
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/spi/PermissionsDao.java b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/spi/PermissionsDao.java
new file mode 100644
index 00000000000..d71b253cf55
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/main/java/org/eclipse/che/multiuser/api/permission/server/spi/PermissionsDao.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.spi;
+
+import java.util.List;
+import java.util.Optional;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.api.permission.server.AbstractPermissionsDomain;
+import org.eclipse.che.multiuser.api.permission.server.model.impl.AbstractPermissions;
+
+/**
+ * General contract of storage for permissions. Single Storage may maintain one or more Domains (it
+ * is responsibility of system on top to make the choice consistent) It actually defines CRUD
+ * methods with some specific such as: - processing list of permissions - checking for existence but
+ * not returning fully qualified stored permission
+ *
+ * @author gazarenkov
+ * @author Sergii Leschenko
+ */
+public interface PermissionsDao {
+
+ /** @return store of domains this storage is able to maintain */
+ AbstractPermissionsDomain getDomain();
+
+ /**
+ * Stores (adds or updates) permissions.
+ *
+ * @param permissions permission to store
+ * @return optional with updated permissions, other way empty optional must be returned
+ * @throws ServerException when any other error occurs during permissions storing
+ */
+ Optional store(T permissions) throws ServerException;
+
+ /**
+ * @param userId user id
+ * @param instanceId instance id
+ * @return user's permissions for specified instance
+ * @throws NullPointerException when instance id is null and domain requires it
+ * @throws NullPointerException when user id is null
+ * @throws NotFoundException when permissions with given user and domain and instance was not
+ * found
+ * @throws ServerException when any other error occurs during permissions fetching
+ */
+ T get(String userId, String instanceId) throws ServerException, NotFoundException;
+
+ /**
+ * @param instanceId instance id
+ * @param maxItems the maximum number of permissions to return
+ * @param skipCount the number of permissions to skip
+ * @return set of permissions
+ * @throws NotFoundException when given instance was not found
+ * @throws ServerException when any other error occurs during permissions fetching
+ */
+ Page getByInstance(String instanceId, int maxItems, long skipCount)
+ throws ServerException, NotFoundException;
+
+ /**
+ * @param userId user id
+ * @return set of permissions
+ * @throws NotFoundException when given instance was not found
+ * @throws ServerException when any other error occurs during permissions fetching
+ */
+ List getByUser(String userId) throws ServerException, NotFoundException;
+
+ /**
+ * @param userId user id
+ * @param instanceId instance id
+ * @param action action name
+ * @return true if the permission exists
+ * @throws ServerException when any other error occurs during permission existence checking
+ */
+ boolean exists(String userId, String instanceId, String action) throws ServerException;
+
+ /**
+ * Removes permissions of user related to the particular instance of specified domain
+ *
+ * @param userId user id
+ * @param instanceId instance id
+ * @throws NotFoundException when permissions with given user and domain and instance was not
+ * found
+ * @throws ServerException when any other error occurs during permissions removing
+ */
+ void remove(String userId, String instanceId) throws ServerException, NotFoundException;
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/test/java/org/eclipse/che/multiuser/api/permission/server/filter/GetPermissionsFilterTest.java b/multiuser/api/che-multiuser-api-permission/src/test/java/org/eclipse/che/multiuser/api/permission/server/filter/GetPermissionsFilterTest.java
new file mode 100644
index 00000000000..9dd770fdcfa
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/test/java/org/eclipse/che/multiuser/api/permission/server/filter/GetPermissionsFilterTest.java
@@ -0,0 +1,200 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.filter;
+
+import static io.restassured.RestAssured.given;
+import static java.util.Collections.singletonList;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD;
+import static org.everrest.assured.JettyHttpServer.SECURE_PATH;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+import io.restassured.response.Response;
+import java.util.List;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.rest.shared.dto.ServiceError;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.api.permission.server.InstanceParameterValidator;
+import org.eclipse.che.multiuser.api.permission.server.PermissionsManager;
+import org.eclipse.che.multiuser.api.permission.server.PermissionsService;
+import org.eclipse.che.multiuser.api.permission.server.SuperPrivilegesChecker;
+import org.eclipse.che.multiuser.api.permission.server.model.impl.AbstractPermissions;
+import org.everrest.assured.EverrestJetty;
+import org.everrest.core.Filter;
+import org.everrest.core.GenericContainerRequest;
+import org.everrest.core.RequestFilter;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link GetPermissionsFilter}
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners(value = {MockitoTestNGListener.class, EverrestJetty.class})
+public class GetPermissionsFilterTest {
+ @SuppressWarnings("unused")
+ private static final EnvironmentFilter FILTER = new EnvironmentFilter();
+
+ @Mock private static Subject subject;
+
+ @Mock private PermissionsManager permissionsManager;
+
+ @Mock private PermissionsService permissionsService;
+
+ @Mock private InstanceParameterValidator instanceValidator;
+
+ @Mock private SuperPrivilegesChecker superPrivilegesChecker;
+
+ @InjectMocks private GetPermissionsFilter permissionsFilter;
+
+ @BeforeMethod
+ public void setUp() {
+ lenient().when(subject.getUserId()).thenReturn("user123");
+ }
+
+ @Test
+ public void shouldRespond403IfUserDoesNotHaveAnyPermissionsForInstance() throws Exception {
+ when(permissionsManager.get("user123", "test", "test123")).thenThrow(new NotFoundException(""));
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .get(SECURE_PATH + "/permissions/test/all?instance=test123");
+
+ assertEquals(response.getStatusCode(), 403);
+ assertEquals(unwrapError(response), "User is not authorized to perform this operation");
+ verifyNoMoreInteractions(permissionsService);
+ verify(instanceValidator).validate("test", "test123");
+ }
+
+ @Test
+ public void shouldRespond400IfInstanceIsNotValid() throws Exception {
+ doThrow(new BadRequestException("instance is not valid"))
+ .when(instanceValidator)
+ .validate(any(), any());
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .get(SECURE_PATH + "/permissions/test/all?instance=test123");
+
+ assertEquals(response.getStatusCode(), 400);
+ assertEquals(unwrapError(response), "instance is not valid");
+ verifyNoMoreInteractions(permissionsService);
+ verify(instanceValidator).validate("test", "test123");
+ }
+
+ @Test
+ public void shouldDoChainIfUserHasAnyPermissionsForInstance() throws Exception {
+ when(permissionsManager.get("user123", "test", "test123"))
+ .thenReturn(new TestPermissions("user123", "test", "test123", singletonList("read")));
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .get(SECURE_PATH + "/permissions/test/all?instance=test123");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(permissionsService).getUsersPermissions(eq("test"), eq("test123"), anyInt(), anyInt());
+ verify(instanceValidator).validate("test", "test123");
+ }
+
+ @Test
+ public void shouldDoChainIfUserDoesNotHaveAnyPermissionsForInstanceButHasSuperPrivileges()
+ throws Exception {
+ when(superPrivilegesChecker.isPrivilegedToManagePermissions(anyString())).thenReturn(true);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .get(SECURE_PATH + "/permissions/test/all?instance=test123");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(permissionsService).getUsersPermissions(eq("test"), eq("test123"), anyInt(), anyInt());
+ verify(superPrivilegesChecker).isPrivilegedToManagePermissions("test");
+ }
+
+ private static String unwrapError(Response response) {
+ return unwrapDto(response, ServiceError.class).getMessage();
+ }
+
+ private static T unwrapDto(Response response, Class dtoClass) {
+ return DtoFactory.getInstance().createDtoFromJson(response.body().print(), dtoClass);
+ }
+
+ @Filter
+ public static class EnvironmentFilter implements RequestFilter {
+ @Override
+ public void doFilter(GenericContainerRequest request) {
+ EnvironmentContext.getCurrent().setSubject(subject);
+ }
+ }
+
+ private class TestPermissions extends AbstractPermissions {
+
+ String domainId;
+ String instanceId;
+ List actions;
+
+ public TestPermissions(
+ String userId, String domainId, String instanceId, List allowedActions) {
+ super(userId);
+ this.domainId = domainId;
+ this.instanceId = instanceId;
+ this.actions = allowedActions;
+ }
+
+ @Override
+ public String getInstanceId() {
+ return instanceId;
+ }
+
+ @Override
+ public String getDomainId() {
+ return domainId;
+ }
+
+ @Override
+ public List getActions() {
+ return actions;
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/test/java/org/eclipse/che/multiuser/api/permission/server/filter/RemovePermissionsFilterTest.java b/multiuser/api/che-multiuser-api-permission/src/test/java/org/eclipse/che/multiuser/api/permission/server/filter/RemovePermissionsFilterTest.java
new file mode 100644
index 00000000000..4d3b063922e
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/test/java/org/eclipse/che/multiuser/api/permission/server/filter/RemovePermissionsFilterTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.filter;
+
+import static io.restassured.RestAssured.given;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD;
+import static org.everrest.assured.JettyHttpServer.SECURE_PATH;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+import io.restassured.response.Response;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.rest.shared.dto.ServiceError;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.api.permission.server.InstanceParameterValidator;
+import org.eclipse.che.multiuser.api.permission.server.PermissionsService;
+import org.eclipse.che.multiuser.api.permission.server.SuperPrivilegesChecker;
+import org.eclipse.che.multiuser.api.permission.server.filter.check.DomainsPermissionsCheckers;
+import org.eclipse.che.multiuser.api.permission.server.filter.check.RemovePermissionsChecker;
+import org.everrest.assured.EverrestJetty;
+import org.everrest.core.Filter;
+import org.everrest.core.GenericContainerRequest;
+import org.everrest.core.RequestFilter;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link RemovePermissionsFilter}
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners(value = {MockitoTestNGListener.class, EverrestJetty.class})
+public class RemovePermissionsFilterTest {
+ @SuppressWarnings("unused")
+ private static final EnvironmentFilter FILTER = new EnvironmentFilter();
+
+ @Mock private static Subject subject;
+
+ @Mock private PermissionsService permissionsService;
+
+ @Mock private InstanceParameterValidator instanceValidator;
+
+ @Mock private SuperPrivilegesChecker superPrivilegesChecker;
+
+ @Mock private DomainsPermissionsCheckers domainsPermissionsCheckers;
+
+ @InjectMocks private RemovePermissionsFilter permissionsFilter;
+
+ @BeforeMethod
+ public void setUp() {
+ lenient().when(subject.getUserId()).thenReturn("user321");
+ }
+
+ @Test
+ public void shouldRespond403IfUserDoesNotHaveAnyPermissionsForInstance() throws Exception {
+ final RemovePermissionsChecker rmPermissionsChecker = mock(RemovePermissionsChecker.class);
+ when(domainsPermissionsCheckers.getRemoveChecker("test")).thenReturn(rmPermissionsChecker);
+ doThrow(new ForbiddenException("ex"))
+ .when(rmPermissionsChecker)
+ .check(anyString(), anyString(), anyString());
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .delete(SECURE_PATH + "/permissions/test?instance=test123&user=user123");
+
+ assertEquals(response.getStatusCode(), 403);
+ verifyNoMoreInteractions(permissionsService);
+ verify(instanceValidator).validate("test", "test123");
+ verify(rmPermissionsChecker, times(1)).check(anyString(), anyString(), anyString());
+ }
+
+ @Test
+ public void shouldRespond400IfInstanceIsNotValid() throws Exception {
+ doThrow(new BadRequestException("instance is not valid"))
+ .when(instanceValidator)
+ .validate(any(), any());
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .delete(SECURE_PATH + "/permissions/test?instance=test123&user=user123");
+
+ assertEquals(response.getStatusCode(), 400);
+ assertEquals(unwrapError(response), "instance is not valid");
+ verifyNoMoreInteractions(permissionsService);
+ verify(instanceValidator).validate("test", "test123");
+ }
+
+ @Test
+ public void shouldDoChainIfUserHasAnyPermissionsForInstance() throws Exception {
+ final RemovePermissionsChecker rmPermissionsChecker = mock(RemovePermissionsChecker.class);
+ when(domainsPermissionsCheckers.getRemoveChecker("test")).thenReturn(rmPermissionsChecker);
+ doNothing().when(rmPermissionsChecker).check("user123", "test", "test123");
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .delete(SECURE_PATH + "/permissions/test?instance=test123&user=user123");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(permissionsService).removePermissions(eq("test"), eq("test123"), eq("user123"));
+ verify(instanceValidator).validate("test", "test123");
+ }
+
+ @Test
+ public void shouldDoChainIfUserTriesToRemoveOwnPermissionsForInstance() throws Exception {
+ when(subject.getUserId()).thenReturn("user123");
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .delete(SECURE_PATH + "/permissions/test?instance=test123&user=user123");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(permissionsService).removePermissions(eq("test"), eq("test123"), eq("user123"));
+ verify(instanceValidator).validate("test", "test123");
+ verify(subject, never()).checkPermission(anyString(), anyString(), anyString());
+ verify(superPrivilegesChecker, never()).isPrivilegedToManagePermissions(anyString());
+ }
+
+ @Test
+ public void shouldDoChainIfUserDoesNotHavePermissionToSetPermissionsButHasSuperPrivileges()
+ throws Exception {
+ when(superPrivilegesChecker.isPrivilegedToManagePermissions(anyString())).thenReturn(true);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .delete(SECURE_PATH + "/permissions/test?instance=test123&user=user123");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(permissionsService).removePermissions(eq("test"), eq("test123"), eq("user123"));
+ verify(superPrivilegesChecker).isPrivilegedToManagePermissions("test");
+ }
+
+ private static String unwrapError(Response response) {
+ return unwrapDto(response, ServiceError.class).getMessage();
+ }
+
+ private static T unwrapDto(Response response, Class dtoClass) {
+ return DtoFactory.getInstance().createDtoFromJson(response.body().print(), dtoClass);
+ }
+
+ @Filter
+ public static class EnvironmentFilter implements RequestFilter {
+ @Override
+ public void doFilter(GenericContainerRequest request) {
+ EnvironmentContext.getCurrent().setSubject(subject);
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/test/java/org/eclipse/che/multiuser/api/permission/server/filter/SetPermissionsFilterTest.java b/multiuser/api/che-multiuser-api-permission/src/test/java/org/eclipse/che/multiuser/api/permission/server/filter/SetPermissionsFilterTest.java
new file mode 100644
index 00000000000..dfa66df19ad
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/test/java/org/eclipse/che/multiuser/api/permission/server/filter/SetPermissionsFilterTest.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.api.permission.server.filter;
+
+import static io.restassured.RestAssured.given;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_NAME;
+import static org.everrest.assured.JettyHttpServer.ADMIN_USER_PASSWORD;
+import static org.everrest.assured.JettyHttpServer.SECURE_PATH;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertEquals;
+
+import io.restassured.response.Response;
+import java.util.Collections;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.ForbiddenException;
+import org.eclipse.che.api.core.rest.shared.dto.ServiceError;
+import org.eclipse.che.commons.env.EnvironmentContext;
+import org.eclipse.che.commons.subject.Subject;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.api.permission.server.InstanceParameterValidator;
+import org.eclipse.che.multiuser.api.permission.server.PermissionsService;
+import org.eclipse.che.multiuser.api.permission.server.SuperPrivilegesChecker;
+import org.eclipse.che.multiuser.api.permission.server.filter.check.DomainsPermissionsCheckers;
+import org.eclipse.che.multiuser.api.permission.server.filter.check.SetPermissionsChecker;
+import org.eclipse.che.multiuser.api.permission.shared.dto.PermissionsDto;
+import org.eclipse.che.multiuser.api.permission.shared.model.Permissions;
+import org.everrest.assured.EverrestJetty;
+import org.everrest.core.Filter;
+import org.everrest.core.GenericContainerRequest;
+import org.everrest.core.RequestFilter;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.testng.MockitoTestNGListener;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.Listeners;
+import org.testng.annotations.Test;
+
+/**
+ * Tests for {@link SetPermissionsFilter}
+ *
+ * @author Sergii Leschenko
+ */
+@Listeners(value = {MockitoTestNGListener.class, EverrestJetty.class})
+public class SetPermissionsFilterTest {
+ @SuppressWarnings("unused")
+ private static final EnvironmentFilter FILTER = new EnvironmentFilter();
+
+ @Mock private static Subject subject;
+
+ @Mock private PermissionsService permissionsService;
+
+ @Mock private SuperPrivilegesChecker superPrivilegesChecker;
+
+ @Mock private InstanceParameterValidator instanceValidator;
+
+ @Mock private DomainsPermissionsCheckers domainsPermissionsCheckers;
+
+ @InjectMocks private SetPermissionsFilter permissionsFilter;
+
+ @BeforeMethod
+ public void setUp() {
+ lenient().when(subject.getUserId()).thenReturn("user123");
+ }
+
+ @Test
+ public void shouldRespond400IfBodyIsNull() throws Exception {
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .when()
+ .post(SECURE_PATH + "/permissions");
+
+ assertEquals(response.getStatusCode(), 400);
+ assertEquals(unwrapError(response), "Permissions descriptor required");
+ verifyNoMoreInteractions(permissionsService);
+ }
+
+ @Test
+ public void shouldRespond400IfDomainIdIsEmpty() throws Exception {
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .body(
+ DtoFactory.newDto(PermissionsDto.class)
+ .withDomainId("")
+ .withInstanceId("test123")
+ .withUserId("user123")
+ .withActions(Collections.singletonList("read")))
+ .when()
+ .post(SECURE_PATH + "/permissions");
+
+ assertEquals(response.getStatusCode(), 400);
+ assertEquals(unwrapError(response), "Domain required");
+ verifyNoMoreInteractions(permissionsService);
+ }
+
+ @Test
+ public void shouldRespond400IfInstanceIsNotValid() throws Exception {
+ doThrow(new BadRequestException("instance is not valid"))
+ .when(instanceValidator)
+ .validate(any(), any());
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .body(
+ DtoFactory.newDto(PermissionsDto.class)
+ .withDomainId("test")
+ .withInstanceId("test123")
+ .withUserId("user123")
+ .withActions(Collections.singletonList("read")))
+ .when()
+ .post(SECURE_PATH + "/permissions");
+
+ assertEquals(response.getStatusCode(), 400);
+ assertEquals(unwrapError(response), "instance is not valid");
+ verifyNoMoreInteractions(permissionsService);
+ verify(instanceValidator).validate("test", "test123");
+ }
+
+ @Test
+ public void shouldRespond403IfUserDoesNotHaveAnyPermissionsForInstance() throws Exception {
+ final SetPermissionsChecker setPermissionsChecker = mock(SetPermissionsChecker.class);
+ when(domainsPermissionsCheckers.getSetChecker("test")).thenReturn(setPermissionsChecker);
+ doThrow(new ForbiddenException("ex")).when(setPermissionsChecker).check(any(Permissions.class));
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .body(
+ DtoFactory.newDto(PermissionsDto.class)
+ .withDomainId("test")
+ .withInstanceId("test123")
+ .withUserId("user123")
+ .withActions(Collections.singletonList("read")))
+ .when()
+ .post(SECURE_PATH + "/permissions");
+
+ assertEquals(response.getStatusCode(), 403);
+ verifyNoMoreInteractions(permissionsService);
+ verify(instanceValidator).validate("test", "test123");
+ verify(setPermissionsChecker, times(1)).check(any(Permissions.class));
+ }
+
+ @Test
+ public void shouldDoChainIfUserHasAnyPermissionsForInstance() throws Exception {
+ final SetPermissionsChecker setPermissionsChecker = mock(SetPermissionsChecker.class);
+ when(domainsPermissionsCheckers.getSetChecker("test")).thenReturn(setPermissionsChecker);
+ doNothing().when(setPermissionsChecker).check(any(Permissions.class));
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .body(
+ DtoFactory.newDto(PermissionsDto.class)
+ .withDomainId("test")
+ .withInstanceId("test123")
+ .withUserId("user123")
+ .withActions(Collections.singletonList("read")))
+ .when()
+ .post(SECURE_PATH + "/permissions");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(permissionsService).storePermissions(any());
+ verify(instanceValidator).validate("test", "test123");
+ }
+
+ @Test
+ public void shouldDoChainIfUserDoesNotHavePermissionToSetPermissionsButHasSuperPrivileges()
+ throws Exception {
+ when(superPrivilegesChecker.isPrivilegedToManagePermissions(anyString())).thenReturn(true);
+
+ final Response response =
+ given()
+ .auth()
+ .basic(ADMIN_USER_NAME, ADMIN_USER_PASSWORD)
+ .contentType("application/json")
+ .body(
+ DtoFactory.newDto(PermissionsDto.class)
+ .withDomainId("test")
+ .withInstanceId("test123")
+ .withUserId("user123")
+ .withActions(Collections.singletonList("read")))
+ .when()
+ .post(SECURE_PATH + "/permissions");
+
+ assertEquals(response.getStatusCode(), 204);
+ verify(permissionsService).storePermissions(any());
+ verify(superPrivilegesChecker).isPrivilegedToManagePermissions("test");
+ }
+
+ private static String unwrapError(Response response) {
+ return unwrapDto(response, ServiceError.class).getMessage();
+ }
+
+ private static T unwrapDto(Response response, Class dtoClass) {
+ return DtoFactory.getInstance().createDtoFromJson(response.body().print(), dtoClass);
+ }
+
+ @Filter
+ public static class EnvironmentFilter implements RequestFilter {
+ @Override
+ public void doFilter(GenericContainerRequest request) {
+ EnvironmentContext.getCurrent().setSubject(subject);
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-permission/src/test/resources/META-INF/services/org.eclipse.che.commons.test.tck.TckModule b/multiuser/api/che-multiuser-api-permission/src/test/resources/META-INF/services/org.eclipse.che.commons.test.tck.TckModule
new file mode 100644
index 00000000000..3d1ea5b0d52
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/test/resources/META-INF/services/org.eclipse.che.commons.test.tck.TckModule
@@ -0,0 +1 @@
+org.eclipse.che.multiuser.api.permission.server.jpa.SystemPermissionsTckModule
diff --git a/multiuser/api/che-multiuser-api-permission/src/test/resources/logback-test.xml b/multiuser/api/che-multiuser-api-permission/src/test/resources/logback-test.xml
new file mode 100644
index 00000000000..c7a7b0b5cf3
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-permission/src/test/resources/logback-test.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+ %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n%nopex
+
+
+
+ target/log/test.log
+
+ %-41(%date[%.15thread]) %-45([%-5level] [%.30logger{30} %L]) - %msg%n
+
+
+
+
+
+
+
+
+
+
diff --git a/multiuser/api/che-multiuser-api-resource-shared/pom.xml b/multiuser/api/che-multiuser-api-resource-shared/pom.xml
new file mode 100644
index 00000000000..4d961d9df7d
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource-shared/pom.xml
@@ -0,0 +1,35 @@
+
+
+
+ 4.0.0
+
+ che-multiuser-api
+ org.eclipse.che.multiuser
+ 7.103.0-SNAPSHOT
+
+ che-multiuser-api-resource-shared
+ jar
+ Che Multiuser :: Resource :: Shared
+
+
+ org.eclipse.che.core
+ che-core-api-dto
+
+
+ org.eclipse.che.core
+ che-core-commons-annotations
+
+
+
diff --git a/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/model/FreeResourcesLimit.java b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/model/FreeResourcesLimit.java
new file mode 100644
index 00000000000..a33d2b911a4
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/model/FreeResourcesLimit.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.model;
+
+import java.util.List;
+
+/**
+ * Represents limit of resources which are available for free usage by some account.
+ *
+ * @author Sergii Leschenko
+ */
+public interface FreeResourcesLimit {
+ /** Returns id of account that can use free resources. */
+ String getAccountId();
+
+ /** Returns resources which are available for free usage. */
+ List extends Resource> getResources();
+}
diff --git a/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/model/ProvidedResources.java b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/model/ProvidedResources.java
new file mode 100644
index 00000000000..3833ab5b5ff
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/model/ProvidedResources.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.model;
+
+import java.util.List;
+import org.eclipse.che.commons.annotation.Nullable;
+
+/**
+ * Resources that are provided for using by account by some resource providing mechanism.
+ *
+ * @author Sergii Leschenko
+ */
+public interface ProvidedResources {
+
+ /** Returns id of resource provider. */
+ String getProviderId();
+
+ /**
+ * Returns id of granted resource entity. Can be null when provider provides static single entry.
+ */
+ @Nullable
+ String getId();
+
+ /** Returns owner of resources. */
+ String getOwner();
+
+ /** Returns time when resources became active. */
+ Long getStartTime();
+
+ /** Returns time when resources will be/became inactive. */
+ Long getEndTime();
+
+ /** Returns list of resources which can be used by owner. */
+ List extends Resource> getResources();
+}
diff --git a/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/model/Resource.java b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/model/Resource.java
new file mode 100644
index 00000000000..552337b871d
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/model/Resource.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.model;
+
+/**
+ * Represents some number of resources that can be used by account.
+ *
+ * @author gazarenkov
+ * @author Sergii Leschenko
+ */
+public interface Resource {
+ /** Returns type of resources, e.g. RAM. */
+ String getType();
+
+ /**
+ * Returns amount of resources.
+ *
+ *
Applicable values here are from -1 to {@link Long#MAX_VALUE} inclusively. -1 value represent
+ * infinity.
+ */
+ long getAmount();
+
+ /** Returns unit of resources. */
+ String getUnit();
+}
diff --git a/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/model/ResourcesDetails.java b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/model/ResourcesDetails.java
new file mode 100644
index 00000000000..d51ea27b063
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/model/ResourcesDetails.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.model;
+
+import java.util.List;
+
+/**
+ * Permits account to use some resources.
+ *
+ * @author gazarenkov
+ * @author Sergii Leschenko
+ */
+public interface ResourcesDetails {
+ /** Returns id of account that is owner of these resources. */
+ String getAccountId();
+
+ /** Returns detailed list of resources which can be used by owner. */
+ List extends ProvidedResources> getProvidedResources();
+
+ /** Returns list of resources which can be used by owner. */
+ List extends Resource> getTotalResources();
+}
diff --git a/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/shared/dto/FreeResourcesLimitDto.java b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/shared/dto/FreeResourcesLimitDto.java
new file mode 100644
index 00000000000..1d155b0c355
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/shared/dto/FreeResourcesLimitDto.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.shared.dto;
+
+import java.util.List;
+import org.eclipse.che.dto.shared.DTO;
+import org.eclipse.che.multiuser.resource.model.FreeResourcesLimit;
+
+/** @author Sergii Leschenko */
+@DTO
+public interface FreeResourcesLimitDto extends FreeResourcesLimit {
+ @Override
+ String getAccountId();
+
+ void setAccountId(String accountId);
+
+ FreeResourcesLimitDto withAccountId(String accountId);
+
+ @Override
+ List getResources();
+
+ void setResources(List resources);
+
+ FreeResourcesLimitDto withResources(List resources);
+}
diff --git a/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/shared/dto/ProvidedResourcesDto.java b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/shared/dto/ProvidedResourcesDto.java
new file mode 100644
index 00000000000..e5a028aee57
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/shared/dto/ProvidedResourcesDto.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.shared.dto;
+
+import java.util.List;
+import org.eclipse.che.dto.shared.DTO;
+import org.eclipse.che.multiuser.resource.model.ProvidedResources;
+
+/** @author Sergii Leschenko */
+@DTO
+public interface ProvidedResourcesDto extends ProvidedResources {
+ @Override
+ String getProviderId();
+
+ void setProviderId(String providerId);
+
+ ProvidedResourcesDto withProviderId(String providerId);
+
+ @Override
+ String getId();
+
+ void setId(String id);
+
+ ProvidedResourcesDto withId(String id);
+
+ @Override
+ String getOwner();
+
+ void setOwner(String owner);
+
+ ProvidedResourcesDto withOwner(String owner);
+
+ @Override
+ Long getStartTime();
+
+ void setStartTime(Long startTime);
+
+ ProvidedResourcesDto withStartTime(Long startTime);
+
+ @Override
+ Long getEndTime();
+
+ void setEndTime(Long endTime);
+
+ ProvidedResourcesDto withEndTime(Long endTime);
+
+ @Override
+ List getResources();
+
+ void setResources(List resources);
+
+ ProvidedResourcesDto withResources(List resources);
+}
diff --git a/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/shared/dto/ResourceDto.java b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/shared/dto/ResourceDto.java
new file mode 100644
index 00000000000..f9e52ed3eed
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/shared/dto/ResourceDto.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.shared.dto;
+
+import org.eclipse.che.dto.shared.DTO;
+import org.eclipse.che.multiuser.resource.model.Resource;
+
+/** @author Sergii Leschenko */
+@DTO
+public interface ResourceDto extends Resource {
+ @Override
+ String getType();
+
+ void setType(String type);
+
+ ResourceDto withType(String type);
+
+ @Override
+ long getAmount();
+
+ void setAmount(long amount);
+
+ ResourceDto withAmount(long amount);
+
+ @Override
+ String getUnit();
+
+ void setUnit(String unit);
+
+ ResourceDto withUnit(String unit);
+}
diff --git a/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/shared/dto/ResourcesDetailsDto.java b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/shared/dto/ResourcesDetailsDto.java
new file mode 100644
index 00000000000..05c85c6cceb
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource-shared/src/main/java/org/eclipse/che/multiuser/resource/shared/dto/ResourcesDetailsDto.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.shared.dto;
+
+import java.util.List;
+import org.eclipse.che.dto.shared.DTO;
+import org.eclipse.che.multiuser.resource.model.ResourcesDetails;
+
+/** @author Sergii Leschenko */
+@DTO
+public interface ResourcesDetailsDto extends ResourcesDetails {
+ @Override
+ String getAccountId();
+
+ void setAccountId(String accountId);
+
+ ResourcesDetailsDto withAccountId(String accountId);
+
+ @Override
+ List getProvidedResources();
+
+ void setProvidedResources(List providedResources);
+
+ ResourcesDetailsDto withProvidedResources(List providedResources);
+
+ @Override
+ List getTotalResources();
+
+ void setTotalResources(List totalResources);
+
+ ResourcesDetailsDto withTotalResources(List totalResources);
+}
diff --git a/multiuser/api/che-multiuser-api-resource/pom.xml b/multiuser/api/che-multiuser-api-resource/pom.xml
new file mode 100644
index 00000000000..5191d3b85fe
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/pom.xml
@@ -0,0 +1,209 @@
+
+
+
+ 4.0.0
+
+ che-multiuser-api
+ org.eclipse.che.multiuser
+ 7.103.0-SNAPSHOT
+
+ che-multiuser-api-resource
+ jar
+ Che Multiuser :: Resource
+
+ ${project.build.directory}/generated-sources/dto/
+
+
+
+ com.google.code.gson
+ gson
+
+
+ com.google.guava
+ guava
+
+
+ com.google.inject
+ guice
+
+
+ io.swagger.core.v3
+ swagger-annotations-jakarta
+
+
+ jakarta.inject
+ jakarta.inject-api
+
+
+ jakarta.ws.rs
+ jakarta.ws.rs-api
+
+
+ org.eclipse.che.core
+ che-core-api-account
+
+
+ org.eclipse.che.core
+ che-core-api-core
+
+
+ org.eclipse.che.core
+ che-core-api-dto
+
+
+ org.eclipse.che.core
+ che-core-api-model
+
+
+ org.eclipse.che.core
+ che-core-api-user
+
+
+ org.eclipse.che.core
+ che-core-api-workspace
+
+
+ org.eclipse.che.core
+ che-core-commons-annotations
+
+
+ org.eclipse.che.core
+ che-core-commons-lang
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-permission
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-resource-shared
+
+
+ org.eclipse.persistence
+ jakarta.persistence
+
+
+ org.slf4j
+ slf4j-api
+
+
+ org.eclipse.persistence
+ org.eclipse.persistence.core
+ provided
+
+
+ org.eclipse.persistence
+ org.eclipse.persistence.jpa
+ provided
+
+
+
+
+
+
+ org.eclipse.che.core
+ che-core-api-dto-maven-plugin
+ ${project.version}
+
+
+ process-sources
+
+ generate
+
+
+
+
+
+ org.eclipse.che.multiuser
+ che-multiuser-api-resource-shared
+ ${project.version}
+
+
+
+
+ org.eclipse.che.multiuser.resource.shared.dto
+
+ ${dto-generator-out-directory}
+ org.eclipse.che.multiuser.resource.api.dto.DtoServerImpls
+ server
+
+
+
+ maven-compiler-plugin
+
+
+ pre-compile
+ generate-sources
+
+ compile
+
+
+
+
+
+ org.codehaus.mojo
+ build-helper-maven-plugin
+
+
+ add-domain
+ process-sources
+
+ add-resource
+
+
+
+
+ ${dto-generator-out-directory}/META-INF
+ META-INF
+
+
+
+
+
+ add-source
+ process-sources
+
+ add-source
+
+
+
+ ${dto-generator-out-directory}
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+
+
+ resource-dependencies
+ process-test-resources
+
+ unpack-dependencies
+
+
+ che-core-sql-schema,
+ che-multiuser-sql-schema
+ che-schema/
+ ${project.build.directory}
+
+
+
+
+
+
+
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/AvailableResourcesProvider.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/AvailableResourcesProvider.java
new file mode 100644
index 00000000000..dc075ce30a6
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/AvailableResourcesProvider.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api;
+
+import java.util.List;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.resource.model.Resource;
+
+/**
+ * Provides resources which are available for usage by account.
+ *
+ *
It can be used for example for implementing resources sharing between accounts or resources
+ * usage limitation when limit should be less than resources provided to account.
+ *
+ * @author Sergii Leschenko
+ */
+public interface AvailableResourcesProvider {
+ /**
+ * Returns resources that are available for usage by account with specified id.
+ *
+ * @param accountId account identifier
+ * @return resources that are available for usage by account with specified id.
+ * @throws NotFoundException when account with specified id was not found
+ * @throws ServerException when some exception occurs
+ */
+ List extends Resource> getAvailableResources(String accountId)
+ throws NotFoundException, ServerException;
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/DtoConverter.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/DtoConverter.java
new file mode 100644
index 00000000000..0f345f559e0
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/DtoConverter.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api;
+
+import java.util.stream.Collectors;
+import org.eclipse.che.dto.server.DtoFactory;
+import org.eclipse.che.multiuser.resource.model.FreeResourcesLimit;
+import org.eclipse.che.multiuser.resource.model.ProvidedResources;
+import org.eclipse.che.multiuser.resource.model.Resource;
+import org.eclipse.che.multiuser.resource.model.ResourcesDetails;
+import org.eclipse.che.multiuser.resource.shared.dto.FreeResourcesLimitDto;
+import org.eclipse.che.multiuser.resource.shared.dto.ProvidedResourcesDto;
+import org.eclipse.che.multiuser.resource.shared.dto.ResourceDto;
+import org.eclipse.che.multiuser.resource.shared.dto.ResourcesDetailsDto;
+
+/**
+ * Helps to convert objects related to resource to DTOs.
+ *
+ * @author Sergii Leschenko
+ */
+public final class DtoConverter {
+ private DtoConverter() {}
+
+ public static ResourceDto asDto(Resource resource) {
+ return DtoFactory.newDto(ResourceDto.class)
+ .withAmount(resource.getAmount())
+ .withType(resource.getType())
+ .withUnit(resource.getUnit());
+ }
+
+ public static FreeResourcesLimitDto asDto(FreeResourcesLimit limit) {
+ return DtoFactory.newDto(FreeResourcesLimitDto.class)
+ .withResources(
+ limit.getResources().stream().map(DtoConverter::asDto).collect(Collectors.toList()))
+ .withAccountId(limit.getAccountId());
+ }
+
+ public static ResourcesDetailsDto asDto(ResourcesDetails resourcesDetails) {
+ return DtoFactory.newDto(ResourcesDetailsDto.class)
+ .withAccountId(resourcesDetails.getAccountId())
+ .withTotalResources(
+ resourcesDetails.getTotalResources().stream()
+ .map(DtoConverter::asDto)
+ .collect(Collectors.toList()))
+ .withProvidedResources(
+ resourcesDetails.getProvidedResources().stream()
+ .map(DtoConverter::asDto)
+ .collect(Collectors.toList()));
+ }
+
+ private static ProvidedResourcesDto asDto(ProvidedResources providedResources) {
+ return DtoFactory.newDto(ProvidedResourcesDto.class)
+ .withId(providedResources.getId())
+ .withOwner(providedResources.getOwner())
+ .withStartTime(providedResources.getStartTime())
+ .withEndTime(providedResources.getEndTime())
+ .withProviderId(providedResources.getProviderId())
+ .withResources(
+ providedResources.getResources().stream()
+ .map(DtoConverter::asDto)
+ .collect(Collectors.toList()));
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourceAggregator.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourceAggregator.java
new file mode 100644
index 00000000000..9f8eb215ea2
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourceAggregator.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api;
+
+import static java.util.stream.Collectors.toSet;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.multiuser.resource.api.exception.NoEnoughResourcesException;
+import org.eclipse.che.multiuser.resource.api.type.ResourceType;
+import org.eclipse.che.multiuser.resource.model.Resource;
+
+/**
+ * Helps aggregate resources by theirs type.
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class ResourceAggregator {
+ private final Map resourcesTypes;
+
+ @Inject
+ public ResourceAggregator(Set resourcesTypes) {
+ this.resourcesTypes =
+ resourcesTypes.stream().collect(Collectors.toMap(ResourceType::getId, Function.identity()));
+ }
+
+ /**
+ * Aggregates resources of the same type.
+ *
+ * @param resources resources list which can contain more that one instance for some type
+ * @return map where key is resources type and value is aggregated resource
+ * @throws IllegalArgumentException when resources list contains resource with not supported type
+ */
+ public Map aggregateByType(List extends Resource> resources) {
+ checkSupporting(resources);
+
+ Map type2Resource = new HashMap<>();
+ for (Resource resource : resources) {
+ final Resource resource1 = type2Resource.get(resource.getType());
+ if (resource1 != null) {
+ type2Resource.put(resource.getType(), aggregate(resource1, resource));
+ } else {
+ type2Resource.put(resource.getType(), resource);
+ }
+ }
+ return type2Resource;
+ }
+
+ /**
+ * Returns list which is result of deduction {@code resourceToDeduct} from {@code
+ * sourceResources}.
+ *
+ * @param sourceResources the source resources
+ * @param resourcesToDeduct the resources which should be deducted from {@code sourceResources}
+ * @throws NoEnoughResourcesException when {@code sourceResources} list doesn't contain enough
+ * resources
+ * @throws IllegalArgumentException when {@code sourceResources} or {@code resourcesToDeduct}
+ * contain resource with not supported type
+ */
+ public List extends Resource> deduct(
+ List extends Resource> sourceResources, List extends Resource> resourcesToDeduct)
+ throws NoEnoughResourcesException {
+ checkSupporting(sourceResources);
+ checkSupporting(resourcesToDeduct);
+
+ final Map result =
+ sourceResources.stream().collect(Collectors.toMap(Resource::getType, Function.identity()));
+ final List missingResources = new ArrayList<>();
+
+ for (Resource toDeduct : resourcesToDeduct) {
+ final Resource sourceResource = result.get(toDeduct.getType());
+ if (sourceResource != null) {
+ try {
+ result.put(toDeduct.getType(), deduct(sourceResource, toDeduct));
+ } catch (NoEnoughResourcesException e) {
+ result.remove(toDeduct.getType());
+ missingResources.addAll(e.getMissingResources());
+ }
+ } else {
+ missingResources.add(toDeduct);
+ }
+ }
+
+ if (!missingResources.isEmpty()) {
+ throw new NoEnoughResourcesException(sourceResources, resourcesToDeduct, missingResources);
+ }
+
+ return new ArrayList<>(result.values());
+ }
+
+ /**
+ * Returns list which contains resources from specified {@code sourceResources} which have
+ * excessive amount in compare to specified {@code resourcesToCompare}.
+ *
+ *
+ *
+ * @param sourceResources the source resources
+ * @param resourcesToCompare the resources which should be compared to {@code sourceResources}
+ * @throws IllegalArgumentException when {@code sourceResources} or {@code resourcesToCompare}
+ * contain resource with not supported type
+ */
+ public List extends Resource> excess(
+ List extends Resource> sourceResources, List extends Resource> resourcesToCompare) {
+ checkSupporting(sourceResources);
+ checkSupporting(resourcesToCompare);
+
+ final Map result =
+ sourceResources.stream().collect(Collectors.toMap(Resource::getType, Function.identity()));
+ for (Resource toCompare : resourcesToCompare) {
+ String resourceType = toCompare.getType();
+ final Resource sourceResource = result.get(resourceType);
+ if (sourceResource != null) {
+ if (sourceResource.getAmount() == toCompare.getAmount()) {
+ // source resource doesn't have excessive amount
+ result.remove(resourceType);
+ continue;
+ }
+ try {
+ Resource excess = deduct(sourceResource, toCompare);
+ if (excess.getAmount() == 0) {
+ // source resource doesn't have excessive amount
+ result.remove(resourceType);
+ } else {
+ result.put(resourceType, excess);
+ }
+ } catch (NoEnoughResourcesException e) {
+ // source resource doesn't have excessive amount
+ result.remove(resourceType);
+ }
+ }
+ }
+
+ return new ArrayList<>(result.values());
+ }
+
+ /**
+ * Aggregates two resources which have the same type.
+ *
+ * @param resourceA resources A
+ * @param resourceB resource B
+ * @return one resources with type {@code T} that is result of aggregating {@code resourceA} and
+ * {@code resourceB}
+ * @throws IllegalArgumentException when {@code T} is not supported type
+ */
+ public Resource aggregate(Resource resourceA, Resource resourceB) {
+ final String typeId = resourceA.getType();
+ final ResourceType resourceType = getResourceType(typeId);
+ return resourceType.aggregate(resourceA, resourceB);
+ }
+
+ /**
+ * Deducts two resources which have the same type.
+ *
+ * @param totalResource total resource
+ * @param deduction resources which should be deducted from {@code totalResource}
+ * @return one resources with type {@code T} that is result of subtraction {@code totalResource}
+ * and {@code deduction}
+ * @throws NoEnoughResourcesException when {@code totalResource}'s amount is less than {@code
+ * deduction}'s amount
+ * @throws IllegalArgumentException when {@code T} is not supported type
+ */
+ public Resource deduct(Resource totalResource, Resource deduction)
+ throws NoEnoughResourcesException {
+ final String typeId = totalResource.getType();
+ final ResourceType resourceType = getResourceType(typeId);
+ return resourceType.deduct(totalResource, deduction);
+ }
+
+ /**
+ * Returns resources list that contains resources with types that are contained by both input
+ * lists.
+ *
+ * @throws IllegalArgumentException when {@code resources} list contains resource with not
+ * supported type
+ */
+ public List extends Resource> intersection(
+ List extends Resource> resourcesA, List extends Resource> resourcesB) {
+ checkSupporting(resourcesA);
+ checkSupporting(resourcesB);
+
+ final Set keysA = resourcesA.stream().map(Resource::getType).collect(toSet());
+ final Set keysB = resourcesB.stream().map(Resource::getType).collect(toSet());
+ final Set commonKeys = ImmutableSet.copyOf(Sets.intersection(keysA, keysB));
+ return Stream.concat(resourcesA.stream(), resourcesB.stream())
+ .filter(res -> commonKeys.contains(res.getType()))
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Returns list that contains one resource with minimum amount for each resource type.
+ *
+ * @throws IllegalArgumentException when {@code resources} list contains resource with not
+ * supported type
+ */
+ public List extends Resource> min(Collection extends Resource> resources) {
+ checkSupporting(resources);
+ Map result = new HashMap<>();
+ for (Resource resource : resources) {
+ String type = resource.getType();
+ Resource min = result.get(type);
+ if (min == null) {
+ result.put(type, resource);
+ } else if (resource.getAmount() != -1) {
+ if (min.getAmount() == -1 || min.getAmount() > resource.getAmount()) {
+ result.put(type, resource);
+ }
+ }
+ }
+ return new ArrayList<>(result.values());
+ }
+
+ /**
+ * Check supporting of all given resources.
+ *
+ * @param resources resources to check types
+ * @throws IllegalArgumentException when {@code resources} list contains resource with not
+ * supported type
+ */
+ private void checkSupporting(Collection extends Resource> resources) {
+ final Set resourcesTypes =
+ resources.stream().map(Resource::getType).collect(Collectors.toSet());
+ for (String resourcesType : resourcesTypes) {
+ if (!this.resourcesTypes.containsKey(resourcesType)) {
+ throw new IllegalArgumentException(
+ String.format("'%s' resource type is not supported", resourcesType));
+ }
+ }
+ }
+
+ /**
+ * Returns resources type by given id.
+ *
+ * @param typeId id of resources type
+ * @return resources type by given id
+ * @throws IllegalArgumentException when type by given id is not supported type
+ */
+ private ResourceType getResourceType(String typeId) {
+ final ResourceType resourceType = resourcesTypes.get(typeId);
+ if (resourceType == null) {
+ throw new IllegalArgumentException(
+ String.format("'%s' resource type is not supported", typeId));
+ }
+ return resourceType;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourceLockKeyProvider.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourceLockKeyProvider.java
new file mode 100644
index 00000000000..09de29423d0
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourceLockKeyProvider.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api;
+
+import org.eclipse.che.api.core.ServerException;
+
+/**
+ * Returns key for fetching lock which will be used for locking resources during resources
+ * operations for account with some type.
+ *
+ * @author Sergii Leschenko
+ */
+public interface ResourceLockKeyProvider {
+ /**
+ * Returns lock key by which resources should be lock during resources operations
+ *
+ * @param accountId account id
+ * @return lock key by which resources should be lock during resources operations
+ * @throws ServerException when any other exception occurs
+ */
+ String getLockKey(String accountId) throws ServerException;
+
+ /** Returns account type for which this class provides locks' ids */
+ String getAccountType();
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourceModule.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourceModule.java
new file mode 100644
index 00000000000..b3ca026423a
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourceModule.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.multibindings.MapBinder;
+import com.google.inject.multibindings.Multibinder;
+import org.eclipse.che.api.workspace.server.WorkspaceManager;
+import org.eclipse.che.multiuser.api.permission.server.account.AccountPermissionsChecker;
+import org.eclipse.che.multiuser.resource.api.free.DefaultResourcesProvider;
+import org.eclipse.che.multiuser.resource.api.free.FreeResourcesProvider;
+import org.eclipse.che.multiuser.resource.api.type.RamResourceType;
+import org.eclipse.che.multiuser.resource.api.type.ResourceType;
+import org.eclipse.che.multiuser.resource.api.type.RuntimeResourceType;
+import org.eclipse.che.multiuser.resource.api.type.TimeoutResourceType;
+import org.eclipse.che.multiuser.resource.api.type.WorkspaceResourceType;
+import org.eclipse.che.multiuser.resource.api.usage.tracker.RamResourceUsageTracker;
+import org.eclipse.che.multiuser.resource.api.usage.tracker.RuntimeResourceUsageTracker;
+import org.eclipse.che.multiuser.resource.api.usage.tracker.WorkspaceResourceUsageTracker;
+import org.eclipse.che.multiuser.resource.api.workspace.LimitsCheckingWorkspaceManager;
+
+/** @author Sergii Leschenko */
+public class ResourceModule extends AbstractModule {
+ @Override
+ protected void configure() {
+
+ bind(WorkspaceManager.class).to(LimitsCheckingWorkspaceManager.class);
+
+ MapBinder.newMapBinder(binder(), String.class, AvailableResourcesProvider.class);
+ Multibinder.newSetBinder(binder(), DefaultResourcesProvider.class);
+ Multibinder.newSetBinder(binder(), ResourceLockKeyProvider.class);
+ Multibinder.newSetBinder(binder(), AccountPermissionsChecker.class);
+
+ Multibinder.newSetBinder(binder(), ResourcesProvider.class)
+ .addBinding()
+ .to(FreeResourcesProvider.class);
+
+ MapBinder.newMapBinder(binder(), String.class, AvailableResourcesProvider.class);
+
+ Multibinder resourcesTypesBinder =
+ Multibinder.newSetBinder(binder(), ResourceType.class);
+ resourcesTypesBinder.addBinding().to(RamResourceType.class);
+ resourcesTypesBinder.addBinding().to(WorkspaceResourceType.class);
+ resourcesTypesBinder.addBinding().to(RuntimeResourceType.class);
+ resourcesTypesBinder.addBinding().to(TimeoutResourceType.class);
+
+ Multibinder usageTrackersBinder =
+ Multibinder.newSetBinder(binder(), ResourceUsageTracker.class);
+ usageTrackersBinder.addBinding().to(RamResourceUsageTracker.class);
+ usageTrackersBinder.addBinding().to(WorkspaceResourceUsageTracker.class);
+ usageTrackersBinder.addBinding().to(RuntimeResourceUsageTracker.class);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourceUsageTracker.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourceUsageTracker.java
new file mode 100644
index 00000000000..9c883d5f5a7
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourceUsageTracker.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api;
+
+import java.util.Optional;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.resource.model.Resource;
+
+/**
+ * Tracks usage of resources of specified type.
+ *
+ * @author Sergii Leschenko
+ */
+public interface ResourceUsageTracker {
+ /**
+ * Returns used resource by given account.
+ *
+ * @param accountId account id to fetch used resource
+ * @return used resource by given account
+ * @throws NotFoundException when account with specified id was not found
+ * @throws ServerException when some exception occurs on used resources fetching
+ */
+ Optional getUsedResource(String accountId) throws NotFoundException, ServerException;
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourcesProvider.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourcesProvider.java
new file mode 100644
index 00000000000..232e0e55798
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/ResourcesProvider.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api;
+
+import java.util.List;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.resource.model.ProvidedResources;
+
+/**
+ * Bridge class that link resources details and resources granting mechanisms.
+ *
+ * @author Sergii Leschenko
+ */
+public interface ResourcesProvider {
+ /**
+ * Returns list of provided resources for given account.
+ *
+ * @param accountId account id
+ * @return list of provided resources for given account or empty list if there are not any
+ * resources for given account
+ * @throws NotFoundException when account with specified id was not found
+ * @throws ServerException when some exception occurs
+ */
+ List getResources(String accountId) throws ServerException, NotFoundException;
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/exception/NoEnoughResourcesException.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/exception/NoEnoughResourcesException.java
new file mode 100644
index 00000000000..0e8664a9d49
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/exception/NoEnoughResourcesException.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.exception;
+
+import static java.util.Collections.singletonList;
+
+import java.util.List;
+import java.util.stream.Collectors;
+import org.eclipse.che.multiuser.resource.model.Resource;
+
+/**
+ * Thrown in case when account doesn't have enough resources to perform some operation.
+ *
+ *
It contains detailed information about resources so required, available, missing amounts to
+ * provide ability to construct user friendly message.
+ *
+ * @author Sergii Leschenko
+ */
+public class NoEnoughResourcesException extends Exception {
+ private static final String MESSAGE =
+ "Account has %s resources to use, but operation requires %s. It requires more %s.";
+
+ private String message;
+ private List extends Resource> availableResources;
+ private List extends Resource> requiredResources;
+ private List extends Resource> missedResources;
+
+ public NoEnoughResourcesException(
+ Resource availableResource, Resource requiredResource, Resource missedResource) {
+ this(
+ singletonList(availableResource),
+ singletonList(requiredResource),
+ singletonList(missedResource));
+ }
+
+ public NoEnoughResourcesException(
+ List extends Resource> availableResources,
+ List extends Resource> requiredResources,
+ List extends Resource> missedResources) {
+ this.availableResources = availableResources;
+ this.requiredResources = requiredResources;
+ this.missedResources = missedResources;
+ }
+
+ @Override
+ public String getMessage() {
+ if (message == null) {
+ message =
+ String.format(
+ MESSAGE,
+ resourcesToString(availableResources),
+ resourcesToString(requiredResources),
+ resourcesToString(missedResources));
+ }
+ return message;
+ }
+
+ public List extends Resource> getRequiredResources() {
+ return requiredResources;
+ }
+
+ public List extends Resource> getAvailableResources() {
+ return availableResources;
+ }
+
+ public List extends Resource> getMissingResources() {
+ return missedResources;
+ }
+
+ private String resourcesToString(List extends Resource> resources) {
+ return '['
+ + resources.stream()
+ .map(resource -> resource.getAmount() + resource.getUnit() + " " + resource.getType())
+ .collect(Collectors.joining(", "))
+ + ']';
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/DefaultResourcesProvider.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/DefaultResourcesProvider.java
new file mode 100644
index 00000000000..eca6a86bf51
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/DefaultResourcesProvider.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.free;
+
+import java.util.List;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl;
+
+/**
+ * Provides default resources which should be are available for usage by account when admin doesn't
+ * override limit by {@link FreeResourcesLimitService}.
+ *
+ * @author Sergii Leschenko
+ */
+public interface DefaultResourcesProvider {
+ /** Provides default resources are available for usage by account */
+ List getResources(String accountId) throws ServerException, NotFoundException;
+
+ /** Returns account type for which this class provides default resources. */
+ String getAccountType();
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/FreeResourcesLimitManager.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/FreeResourcesLimitManager.java
new file mode 100644
index 00000000000..bb669c42cc9
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/FreeResourcesLimitManager.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.free;
+
+import static java.util.Objects.requireNonNull;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.resource.model.FreeResourcesLimit;
+import org.eclipse.che.multiuser.resource.spi.FreeResourcesLimitDao;
+import org.eclipse.che.multiuser.resource.spi.impl.FreeResourcesLimitImpl;
+
+/**
+ * Facade for free resources limit related operations.
+ *
+ * @author Sergii Leschenko
+ */
+// TODO Add checking resources availability before limit changing and removing
+@Singleton
+public class FreeResourcesLimitManager {
+ private final FreeResourcesLimitDao freeResourcesLimitDao;
+
+ @Inject
+ public FreeResourcesLimitManager(FreeResourcesLimitDao freeResourcesLimitDao) {
+ this.freeResourcesLimitDao = freeResourcesLimitDao;
+ }
+
+ /**
+ * Stores (creates new one or updates existed) free resource limit.
+ *
+ * @param freeResourcesLimit resources limit to store
+ * @return stored resources limit
+ * @throws NullPointerException when {@code freeResourcesLimit} is null
+ * @throws NotFoundException when resources limit contains resource with non supported type
+ * @throws ConflictException when the specified account doesn't exist
+ * @throws ServerException when any other error occurs
+ */
+ public FreeResourcesLimit store(FreeResourcesLimit freeResourcesLimit)
+ throws NotFoundException, ConflictException, ServerException {
+ requireNonNull(freeResourcesLimit, "Required non-null free resources limit");
+ final FreeResourcesLimitImpl toStore = new FreeResourcesLimitImpl(freeResourcesLimit);
+ freeResourcesLimitDao.store(toStore);
+ return toStore;
+ }
+
+ /**
+ * Returns free resources limit for account with specified id.
+ *
+ * @param accountId account id to fetch resources limit
+ * @return free resources limit for account with specified id
+ * @throws NullPointerException when {@code accountId} is null
+ * @throws NotFoundException when free resources limit for specifies id was not found
+ * @throws ServerException when any other error occurs
+ */
+ public FreeResourcesLimit get(String accountId) throws NotFoundException, ServerException {
+ requireNonNull(accountId, "Required non-null account id");
+ return freeResourcesLimitDao.get(accountId);
+ }
+
+ /**
+ * Removes free resources limit for account with specified id.
+ *
+ *
After removing resources limit account will be able to use default resources
+ *
+ *
Doesn't throw an exception when resources limit for specified {@code accountId} does not
+ * exist
+ *
+ * @param accountId account id to remove resources limit
+ * @throws NullPointerException when {@code accountId} is null
+ * @throws ServerException when any other error occurs
+ */
+ public void remove(String accountId) throws ServerException {
+ requireNonNull(accountId, "Required non-null account id");
+ freeResourcesLimitDao.remove(accountId);
+ }
+
+ /**
+ * Gets all free resources limits.
+ *
+ * @param maxItems the maximum number of limits to return
+ * @param skipCount the number of limits to skip
+ * @return list of limits POJO or empty list if no limits were found
+ * @throws ServerException when any other error occurs
+ */
+ public Page extends FreeResourcesLimit> getAll(int maxItems, int skipCount)
+ throws ServerException {
+ return freeResourcesLimitDao.getAll(maxItems, skipCount);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/FreeResourcesLimitService.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/FreeResourcesLimitService.java
new file mode 100644
index 00000000000..f9e394d3943
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/FreeResourcesLimitService.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.free;
+
+import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.ws.rs.Consumes;
+import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.DefaultValue;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.QueryParam;
+import jakarta.ws.rs.core.Response;
+import javax.inject.Inject;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.Page;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.rest.Service;
+import org.eclipse.che.multiuser.resource.api.DtoConverter;
+import org.eclipse.che.multiuser.resource.model.FreeResourcesLimit;
+import org.eclipse.che.multiuser.resource.shared.dto.FreeResourcesLimitDto;
+
+/**
+ * Defines REST API for managing of free resources limits
+ *
+ * @author Sergii Leschenko
+ */
+@Deprecated
+@Tag(name = "resource-free", description = "Free resources limit REST API")
+@Path("/resource/free")
+public class FreeResourcesLimitService extends Service {
+ private final FreeResourcesLimitManager freeResourcesLimitManager;
+ private final FreeResourcesLimitValidator freeResourcesLimitValidator;
+
+ @Inject
+ public FreeResourcesLimitService(
+ FreeResourcesLimitValidator freeResourcesLimitValidator,
+ FreeResourcesLimitManager freeResourcesLimitManager) {
+ this.freeResourcesLimitManager = freeResourcesLimitManager;
+ this.freeResourcesLimitValidator = freeResourcesLimitValidator;
+ }
+
+ @POST
+ @Consumes(APPLICATION_JSON)
+ @Produces(APPLICATION_JSON)
+ @Operation(
+ summary = "Store free resources limit",
+ responses = {
+ @ApiResponse(
+ responseCode = "201",
+ description = "The resources limit successfully stored",
+ content = @Content(schema = @Schema(implementation = FreeResourcesLimitDto.class))),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Missed required parameters, parameters are not valid"),
+ @ApiResponse(responseCode = "409", description = "The specified account doesn't exist"),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public Response storeFreeResourcesLimit(
+ @Parameter(description = "Free resources limit") FreeResourcesLimitDto resourcesLimit)
+ throws BadRequestException, NotFoundException, ConflictException, ServerException {
+ freeResourcesLimitValidator.check(resourcesLimit);
+ return Response.status(201)
+ .entity(DtoConverter.asDto(freeResourcesLimitManager.store(resourcesLimit)))
+ .build();
+ }
+
+ @GET
+ @Produces(APPLICATION_JSON)
+ @Operation(
+ summary = "Get free resources limits",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The resources limits successfully fetched",
+ content =
+ @Content(
+ array =
+ @ArraySchema(
+ schema = @Schema(implementation = FreeResourcesLimitDto.class)))),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public Response getFreeResourcesLimits(
+ @Parameter(description = "Max items") @QueryParam("maxItems") @DefaultValue("30")
+ int maxItems,
+ @Parameter(description = "Skip count") @QueryParam("skipCount") @DefaultValue("0")
+ int skipCount)
+ throws ServerException {
+
+ final Page extends FreeResourcesLimit> limitsPage =
+ freeResourcesLimitManager.getAll(maxItems, skipCount);
+
+ return Response.ok()
+ .entity(limitsPage.getItems(DtoConverter::asDto))
+ .header("Link", createLinkHeader(limitsPage))
+ .build();
+ }
+
+ @GET
+ @Path("/{accountId}")
+ @Produces(APPLICATION_JSON)
+ @Operation(
+ summary = "Get free resources limit for account with given id",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The resources limit successfully fetched",
+ content = @Content(schema = @Schema(implementation = FreeResourcesLimitDto.class))),
+ @ApiResponse(
+ responseCode = "400",
+ description = "Missed required parameters, parameters are not valid"),
+ @ApiResponse(
+ responseCode = "404",
+ description = "Resources limit for given account was not found"),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public FreeResourcesLimitDto getFreeResourcesLimit(
+ @Parameter(description = "Account id") @PathParam("accountId") String accountId)
+ throws BadRequestException, NotFoundException, ServerException {
+ return DtoConverter.asDto(freeResourcesLimitManager.get(accountId));
+ }
+
+ @DELETE
+ @Path("/{accountId}")
+ @Operation(
+ summary = "Remove free resources limit for account with given id",
+ responses = {
+ @ApiResponse(
+ responseCode = "204",
+ description = "The resources limit successfully removed",
+ content = @Content(schema = @Schema(implementation = FreeResourcesLimitDto.class))),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public void removeFreeResourcesLimit(
+ @Parameter(description = "Account id") @PathParam("accountId") String accountId)
+ throws ServerException {
+ freeResourcesLimitManager.remove(accountId);
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/FreeResourcesLimitValidator.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/FreeResourcesLimitValidator.java
new file mode 100644
index 00000000000..31bf222ff99
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/FreeResourcesLimitValidator.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.free;
+
+import static java.lang.String.format;
+
+import java.util.HashSet;
+import java.util.Set;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.multiuser.resource.model.FreeResourcesLimit;
+import org.eclipse.che.multiuser.resource.shared.dto.FreeResourcesLimitDto;
+import org.eclipse.che.multiuser.resource.shared.dto.ResourceDto;
+
+/**
+ * Utils for validation of {@link FreeResourcesLimit}
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class FreeResourcesLimitValidator {
+ private final ResourceValidator resourceValidator;
+
+ @Inject
+ public FreeResourcesLimitValidator(ResourceValidator resourceValidator) {
+ this.resourceValidator = resourceValidator;
+ }
+
+ /**
+ * Validates given {@code freeResourcesLimit}
+ *
+ * @param freeResourcesLimit resources limit to validate
+ * @throws BadRequestException when {@code freeResourcesLimit} is null
+ * @throws BadRequestException when any of {@code freeResourcesLimit.getResources} is not valid
+ * @see ResourceValidator#validate(ResourceDto)
+ */
+ public void check(FreeResourcesLimitDto freeResourcesLimit) throws BadRequestException {
+ if (freeResourcesLimit == null) {
+ throw new BadRequestException("Missed free resources limit description.");
+ }
+ if (freeResourcesLimit.getAccountId() == null) {
+ throw new BadRequestException("Missed account id.");
+ }
+
+ Set resourcesToSet = new HashSet<>();
+ for (ResourceDto resource : freeResourcesLimit.getResources()) {
+ if (!resourcesToSet.add(resource.getType())) {
+ throw new BadRequestException(
+ format(
+ "Free resources limit should contain only one resources with type '%s'.",
+ resource.getType()));
+ }
+ resourceValidator.validate(resource);
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/FreeResourcesProvider.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/FreeResourcesProvider.java
new file mode 100644
index 00000000000..74326447e08
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/FreeResourcesProvider.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.free;
+
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonList;
+import static java.util.stream.Collectors.toMap;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.account.api.AccountManager;
+import org.eclipse.che.account.shared.model.Account;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.resource.api.ResourcesProvider;
+import org.eclipse.che.multiuser.resource.model.FreeResourcesLimit;
+import org.eclipse.che.multiuser.resource.model.ProvidedResources;
+import org.eclipse.che.multiuser.resource.model.Resource;
+import org.eclipse.che.multiuser.resource.spi.impl.ProvidedResourcesImpl;
+import org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl;
+
+/**
+ * Provides free resources for account usage.
+ *
+ *
Returns free resources limits if it is specified for given account or default free resources
+ * limit in other case
+ *
+ *
Default resources should be provided by {@link DefaultResourcesProvider} for different account
+ * types
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class FreeResourcesProvider implements ResourcesProvider {
+ public static final String FREE_RESOURCES_PROVIDER = "free";
+
+ private final FreeResourcesLimitManager freeResourcesLimitManager;
+ private final AccountManager accountManager;
+ private final Map defaultResourcesProviders;
+
+ @Inject
+ public FreeResourcesProvider(
+ FreeResourcesLimitManager freeResourcesLimitManager,
+ AccountManager accountManager,
+ Set defaultResourcesProviders) {
+ this.freeResourcesLimitManager = freeResourcesLimitManager;
+ this.accountManager = accountManager;
+ this.defaultResourcesProviders =
+ defaultResourcesProviders.stream()
+ .collect(toMap(DefaultResourcesProvider::getAccountType, Function.identity()));
+ }
+
+ @Override
+ public List getResources(String accountId)
+ throws ServerException, NotFoundException {
+ Map freeResources = new HashMap<>();
+ String limitId = null;
+ try {
+ FreeResourcesLimit resourcesLimit = freeResourcesLimitManager.get(accountId);
+ for (Resource resource : resourcesLimit.getResources()) {
+ freeResources.put(resource.getType(), new ResourceImpl(resource));
+ }
+ limitId = resourcesLimit.getAccountId();
+ } catch (NotFoundException ignored) {
+ // there is no resources limit for given account
+ }
+
+ // add default resources which are not specified by limit
+ for (ResourceImpl resource : getDefaultResources(accountId)) {
+ freeResources.putIfAbsent(resource.getType(), resource);
+ }
+
+ if (!freeResources.isEmpty()) {
+ return singletonList(
+ new ProvidedResourcesImpl(
+ FREE_RESOURCES_PROVIDER, limitId, accountId, -1L, -1L, freeResources.values()));
+ } else {
+ return emptyList();
+ }
+ }
+
+ private List getDefaultResources(String accountId)
+ throws NotFoundException, ServerException {
+ List defaultResources = new ArrayList<>();
+ final Account account = accountManager.getById(accountId);
+
+ final DefaultResourcesProvider defaultResourcesProvider =
+ defaultResourcesProviders.get(account.getType());
+ if (defaultResourcesProvider != null) {
+ defaultResources.addAll(defaultResourcesProvider.getResources(accountId));
+ }
+
+ return defaultResources;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/ResourceValidator.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/ResourceValidator.java
new file mode 100644
index 00000000000..05dd3b0d491
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/free/ResourceValidator.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.free;
+
+import static java.util.stream.Collectors.toMap;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.BadRequestException;
+import org.eclipse.che.multiuser.resource.api.type.ResourceType;
+import org.eclipse.che.multiuser.resource.model.Resource;
+import org.eclipse.che.multiuser.resource.shared.dto.ResourceDto;
+
+/**
+ * Utils for validation of {@link Resource}.
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class ResourceValidator {
+ private final Map> resourcesTypesToUnits;
+ private final Map resourcesTypesToDefaultUnit;
+
+ @Inject
+ public ResourceValidator(Set supportedResources) {
+ this.resourcesTypesToUnits =
+ supportedResources.stream()
+ .collect(toMap(ResourceType::getId, ResourceType::getSupportedUnits));
+ this.resourcesTypesToDefaultUnit =
+ supportedResources.stream()
+ .collect(toMap(ResourceType::getId, ResourceType::getDefaultUnit));
+ }
+
+ /**
+ * Validates given {@code resource}
+ *
+ *
{@link ResourceDto#getUnit()} can be null then {@link ResourceType#getDefaultUnit() default
+ * unit} of {@link ResourceDto#getType() specified type} will be set.
+ *
+ * @param resource resource to validate
+ * @throws BadRequestException when {@code resource} is null
+ * @throws BadRequestException when {@code resource} has non supported type
+ * @throws BadRequestException when {@code resource} has non supported unit
+ */
+ public void validate(ResourceDto resource) throws BadRequestException {
+ if (resource == null) {
+ throw new BadRequestException("Missed resource");
+ }
+
+ final Set units = resourcesTypesToUnits.get(resource.getType());
+
+ if (units == null) {
+ throw new BadRequestException(
+ "Specified resources type '" + resource.getType() + "' is not supported");
+ }
+
+ if (resource.getUnit() == null) {
+ resource.setUnit(resourcesTypesToDefaultUnit.get(resource.getType()));
+ } else {
+ if (!units.contains(resource.getUnit())) {
+ throw new BadRequestException(
+ "Specified resources type '"
+ + resource.getType()
+ + "' support only following units: "
+ + units.stream().collect(Collectors.joining(", ")));
+ }
+ }
+
+ if (resource.getAmount() < -1) {
+ throw new BadRequestException(
+ "Resources with type '" + resource.getType() + "' has negative amount");
+ }
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/AbstractExhaustibleResource.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/AbstractExhaustibleResource.java
new file mode 100644
index 00000000000..31372025e09
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/AbstractExhaustibleResource.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.type;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import org.eclipse.che.multiuser.resource.api.exception.NoEnoughResourcesException;
+import org.eclipse.che.multiuser.resource.model.Resource;
+import org.eclipse.che.multiuser.resource.spi.impl.ResourceImpl;
+
+/**
+ * Abstract resource that contains logic for aggregating and deduction for exhaustible resources.
+ *
+ * @author Sergii Leschenko
+ */
+public abstract class AbstractExhaustibleResource implements ResourceType {
+ @Override
+ public Resource aggregate(Resource resourceA, Resource resourceB) {
+ checkResource(resourceA);
+ checkResource(resourceB);
+
+ if (resourceA.getAmount() == -1 || resourceB.getAmount() == -1) {
+ return new ResourceImpl(getId(), -1, getDefaultUnit());
+ }
+
+ return new ResourceImpl(
+ getId(), resourceA.getAmount() + resourceB.getAmount(), getDefaultUnit());
+ }
+
+ @Override
+ public Resource deduct(Resource total, Resource deduction) throws NoEnoughResourcesException {
+ checkResource(total);
+ checkResource(deduction);
+
+ if (total.getAmount() == -1) {
+ return total;
+ }
+
+ if (deduction.getAmount() == -1) {
+ throw new NoEnoughResourcesException(total, deduction, deduction);
+ }
+
+ final long resultAmount = total.getAmount() - deduction.getAmount();
+ if (resultAmount < 0) {
+ throw new NoEnoughResourcesException(
+ total, deduction, new ResourceImpl(getId(), -resultAmount, getDefaultUnit()));
+ }
+ return new ResourceImpl(getId(), resultAmount, getDefaultUnit());
+ }
+
+ /**
+ * Checks that given resources can be processed by this resource type
+ *
+ * @param resource resource to check
+ * @throws IllegalArgumentException if given resources has unsupported type or unit
+ */
+ private void checkResource(Resource resource) {
+ checkArgument(
+ getId().equals(resource.getType()), "Resource should have '" + getId() + "' type");
+ checkArgument(
+ getSupportedUnits().contains(resource.getUnit()),
+ "Resource has unsupported unit '" + resource.getUnit() + "'");
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/RamResourceType.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/RamResourceType.java
new file mode 100644
index 00000000000..0648b129360
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/RamResourceType.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.type;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+
+/**
+ * Describes resource type that control number of RAM which can be used by running workspaces at the
+ * same time.
+ *
+ * @author Sergii Leschenko
+ */
+public class RamResourceType extends AbstractExhaustibleResource {
+ public static final String ID = "RAM";
+ public static final String UNIT = "mb";
+
+ private static final Set SUPPORTED_UNITS = ImmutableSet.of(UNIT);
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Number of RAM which can be used by running workspaces at the same time";
+ }
+
+ @Override
+ public Set getSupportedUnits() {
+ return SUPPORTED_UNITS;
+ }
+
+ @Override
+ public String getDefaultUnit() {
+ return UNIT;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/ResourceType.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/ResourceType.java
new file mode 100644
index 00000000000..049a514fb81
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/ResourceType.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.type;
+
+import java.util.Set;
+import org.eclipse.che.multiuser.resource.api.exception.NoEnoughResourcesException;
+import org.eclipse.che.multiuser.resource.model.Resource;
+
+/**
+ * Represents some kind of resources which can be used by account.
+ *
+ * @author Sergii Leschenko
+ */
+public interface ResourceType {
+ /** Returns id of resource type. */
+ String getId();
+
+ /** Returns description of resource type. */
+ String getDescription();
+
+ /** Returns supported units. */
+ Set getSupportedUnits();
+
+ /** Returns default unit. */
+ String getDefaultUnit();
+
+ /**
+ * Defines function for aggregating two resources of this type.
+ *
+ * @param resourceA resources A
+ * @param resourceB resource B
+ * @throws IllegalArgumentException if one of resources has unsupported type or unit
+ */
+ Resource aggregate(Resource resourceA, Resource resourceB);
+
+ /**
+ * Defines function for subtraction two resources of this type.
+ *
+ * @param total total resource
+ * @param deduction resource that should be deducted from {@code total}
+ * @throws IllegalArgumentException if one of resources has unsupported type or unit
+ * @throws NoEnoughResourcesException when {@code total}'s amount is less than {@code deduction}'s
+ * amount
+ */
+ Resource deduct(Resource total, Resource deduction) throws NoEnoughResourcesException;
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/RuntimeResourceType.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/RuntimeResourceType.java
new file mode 100644
index 00000000000..1f96501f887
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/RuntimeResourceType.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.type;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+
+/**
+ * Describes resource type that control number of workspaces which user can run at the same time.
+ *
+ * @author Sergii Leshchenko
+ */
+public class RuntimeResourceType extends AbstractExhaustibleResource {
+ public static final String ID = "runtime";
+ public static final String UNIT = "item";
+
+ private static final Set SUPPORTED_UNITS = ImmutableSet.of(UNIT);
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Number of workspaces which user can run at the same time";
+ }
+
+ @Override
+ public Set getSupportedUnits() {
+ return SUPPORTED_UNITS;
+ }
+
+ @Override
+ public String getDefaultUnit() {
+ return UNIT;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/TimeoutResourceType.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/TimeoutResourceType.java
new file mode 100644
index 00000000000..140148c4298
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/TimeoutResourceType.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.type;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+import org.eclipse.che.multiuser.resource.api.exception.NoEnoughResourcesException;
+import org.eclipse.che.multiuser.resource.model.Resource;
+
+/**
+ * Describes resource type that control the length of time that a user is idle with their workspace
+ * when the system will suspend the workspace by snapshotting it and then stopping it.
+ *
+ * @author Sergii Leschenko
+ */
+public class TimeoutResourceType implements ResourceType {
+ public static final String ID = "timeout";
+ public static final String UNIT = "minute";
+
+ private static final Set SUPPORTED_UNITS = ImmutableSet.of(UNIT);
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Timeout";
+ }
+
+ @Override
+ public Set getSupportedUnits() {
+ return SUPPORTED_UNITS;
+ }
+
+ @Override
+ public String getDefaultUnit() {
+ return UNIT;
+ }
+
+ @Override
+ public Resource aggregate(Resource resourceA, Resource resourceB) {
+ return resourceA.getAmount() > resourceB.getAmount() ? resourceA : resourceB;
+ }
+
+ @Override
+ public Resource deduct(Resource total, Resource deduction) throws NoEnoughResourcesException {
+ return total;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/WorkspaceResourceType.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/WorkspaceResourceType.java
new file mode 100644
index 00000000000..818919a01d9
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/type/WorkspaceResourceType.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.type;
+
+import com.google.common.collect.ImmutableSet;
+import java.util.Set;
+
+/**
+ * Describes resource type that control number of workspaces which user can have at the same time.
+ *
+ * @author Sergii Leshchenko
+ */
+public class WorkspaceResourceType extends AbstractExhaustibleResource {
+ public static final String ID = "workspace";
+ public static final String UNIT = "item";
+
+ private static final Set SUPPORTED_UNITS = ImmutableSet.of(UNIT);
+
+ @Override
+ public String getId() {
+ return ID;
+ }
+
+ @Override
+ public String getDescription() {
+ return "Number of workspaces which user can have at the same time";
+ }
+
+ @Override
+ public Set getSupportedUnits() {
+ return SUPPORTED_UNITS;
+ }
+
+ @Override
+ public String getDefaultUnit() {
+ return UNIT;
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/usage/DefaultAvailableResourcesProvider.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/usage/DefaultAvailableResourcesProvider.java
new file mode 100644
index 00000000000..809b319e9be
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/usage/DefaultAvailableResourcesProvider.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.usage;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.resource.api.AvailableResourcesProvider;
+import org.eclipse.che.multiuser.resource.api.ResourceAggregator;
+import org.eclipse.che.multiuser.resource.api.exception.NoEnoughResourcesException;
+import org.eclipse.che.multiuser.resource.model.Resource;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Default implementation of providing available resources for accounts.
+ *
+ *
By default account can use resources only by itself, so available resources equals to total
+ * resources minus resources which are already used by account.
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class DefaultAvailableResourcesProvider implements AvailableResourcesProvider {
+ private static final Logger LOG =
+ LoggerFactory.getLogger(DefaultAvailableResourcesProvider.class);
+
+ private final Provider resourceManagerProvider;
+ private final ResourceAggregator resourceAggregator;
+
+ @Inject
+ public DefaultAvailableResourcesProvider(
+ Provider resourceManagerProvider, ResourceAggregator resourceAggregator) {
+ this.resourceManagerProvider = resourceManagerProvider;
+ this.resourceAggregator = resourceAggregator;
+ }
+
+ @Override
+ public List extends Resource> getAvailableResources(String accountId)
+ throws NotFoundException, ServerException {
+ ResourceManager resourceManager = resourceManagerProvider.get();
+ List extends Resource> totalResources = null;
+ List usedResources = null;
+ try {
+ totalResources = resourceManager.getTotalResources(accountId);
+ usedResources = new ArrayList<>(resourceManager.getUsedResources(accountId));
+ return resourceAggregator.deduct(totalResources, usedResources);
+ } catch (NoEnoughResourcesException e) {
+ LOG.warn(
+ "Account with id {} uses more resources {} than he has {}.",
+ accountId,
+ format(usedResources),
+ format(totalResources));
+ return resourceAggregator.excess(totalResources, usedResources);
+ }
+ }
+
+ /** Returns formatted string for list of resources. */
+ private static String format(Collection extends Resource> resources) {
+ return '['
+ + resources.stream()
+ .map(
+ resource -> resource.getAmount() + resource.getUnit() + " of " + resource.getType())
+ .collect(Collectors.joining(", "))
+ + ']';
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/usage/ResourceManager.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/usage/ResourceManager.java
new file mode 100644
index 00000000000..efc75cd7f0f
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/usage/ResourceManager.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.usage;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import org.eclipse.che.account.api.AccountManager;
+import org.eclipse.che.account.shared.model.Account;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.multiuser.resource.api.AvailableResourcesProvider;
+import org.eclipse.che.multiuser.resource.api.ResourceAggregator;
+import org.eclipse.che.multiuser.resource.api.ResourceUsageTracker;
+import org.eclipse.che.multiuser.resource.api.ResourcesProvider;
+import org.eclipse.che.multiuser.resource.api.exception.NoEnoughResourcesException;
+import org.eclipse.che.multiuser.resource.model.ProvidedResources;
+import org.eclipse.che.multiuser.resource.model.Resource;
+import org.eclipse.che.multiuser.resource.model.ResourcesDetails;
+import org.eclipse.che.multiuser.resource.spi.impl.ResourcesDetailsImpl;
+
+/**
+ * Facade for resources related operations.
+ *
+ * @author Sergii Leschenko
+ */
+@Singleton
+public class ResourceManager {
+ private final ResourceAggregator resourceAggregator;
+ private final Set resourcesProviders;
+ private final Set usageTrackers;
+ private final AccountManager accountManager;
+ private final Map accountTypeToAvailableResourcesProvider;
+ private final DefaultAvailableResourcesProvider defaultAvailableResourcesProvider;
+
+ @Inject
+ public ResourceManager(
+ ResourceAggregator resourceAggregator,
+ Set resourcesProviders,
+ Set usageTrackers,
+ AccountManager accountManager,
+ Map accountTypeToAvailableResourcesProvider,
+ DefaultAvailableResourcesProvider defaultAvailableResourcesProvider) {
+ this.resourceAggregator = resourceAggregator;
+ this.resourcesProviders = resourcesProviders;
+ this.usageTrackers = usageTrackers;
+ this.accountManager = accountManager;
+ this.accountTypeToAvailableResourcesProvider = accountTypeToAvailableResourcesProvider;
+ this.defaultAvailableResourcesProvider = defaultAvailableResourcesProvider;
+ }
+
+ /**
+ * Returns list of resources which are available for usage by given account.
+ *
+ * @param accountId id of account
+ * @return list of resources which are available for usage by given account
+ * @throws NotFoundException when account with specified id was not found
+ * @throws ServerException when some exception occurred while resources fetching
+ */
+ public List extends Resource> getTotalResources(String accountId)
+ throws NotFoundException, ServerException {
+ return getResourceDetails(accountId).getTotalResources();
+ }
+
+ /**
+ * Returns list of resources which are available for usage by given account.
+ *
+ * @param accountId id of account
+ * @return list of resources which are available for usage by given account
+ * @throws NotFoundException when account with specified id was not found
+ * @throws ServerException when some exception occurred while resources fetching
+ */
+ public List extends Resource> getAvailableResources(String accountId)
+ throws NotFoundException, ServerException {
+ final Account account = accountManager.getById(accountId);
+ final AvailableResourcesProvider availableResourcesProvider =
+ accountTypeToAvailableResourcesProvider.get(account.getType());
+
+ if (availableResourcesProvider == null) {
+ return defaultAvailableResourcesProvider.getAvailableResources(accountId);
+ }
+
+ return availableResourcesProvider.getAvailableResources(accountId);
+ }
+
+ /**
+ * Returns list of resources which are used by given account.
+ *
+ * @param accountId id of account
+ * @return list of resources which are used by given account
+ * @throws NotFoundException when account with specified id was not found
+ * @throws ServerException when some exception occurred while resources fetching
+ */
+ public List extends Resource> getUsedResources(String accountId)
+ throws NotFoundException, ServerException {
+ List usedResources = new ArrayList<>();
+ for (ResourceUsageTracker usageTracker : usageTrackers) {
+ Optional usedResource = usageTracker.getUsedResource(accountId);
+ usedResource.ifPresent(usedResources::add);
+ }
+ return usedResources;
+ }
+
+ /**
+ * Checks that specified account has available resources to use
+ *
+ * @param accountId account id
+ * @param resources resources to check availability
+ * @throws NotFoundException when account with specified id was not found
+ * @throws NoEnoughResourcesException when account doesn't have specified available resources
+ * @throws ServerException when any other error occurs
+ */
+ public void checkResourcesAvailability(String accountId, List extends Resource> resources)
+ throws NotFoundException, NoEnoughResourcesException, ServerException {
+ List extends Resource> availableResources = getAvailableResources(accountId);
+ // check resources availability
+ resourceAggregator.deduct(availableResources, resources);
+ }
+
+ /**
+ * Returns detailed information about resources which given account can use.
+ *
+ * @param accountId account id
+ * @return detailed information about resources which can be used by given account
+ * @throws NotFoundException when account with specified id was not found
+ * @throws ServerException when some exception occurs
+ */
+ public ResourcesDetails getResourceDetails(String accountId)
+ throws NotFoundException, ServerException {
+ final List resources = new ArrayList<>();
+ for (ResourcesProvider resourcesProvider : resourcesProviders) {
+ resources.addAll(resourcesProvider.getResources(accountId));
+ }
+
+ final List allResources =
+ resources.stream()
+ .flatMap(providedResources -> providedResources.getResources().stream())
+ .collect(Collectors.toList());
+
+ return new ResourcesDetailsImpl(
+ accountId,
+ resources,
+ new ArrayList<>(resourceAggregator.aggregateByType(allResources).values()));
+ }
+}
diff --git a/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/usage/ResourceService.java b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/usage/ResourceService.java
new file mode 100644
index 00000000000..96466769391
--- /dev/null
+++ b/multiuser/api/che-multiuser-api-resource/src/main/java/org/eclipse/che/multiuser/resource/api/usage/ResourceService.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (c) 2012-2025 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.multiuser.resource.api.usage;
+
+import static jakarta.ws.rs.core.MediaType.APPLICATION_JSON;
+import static org.eclipse.che.multiuser.resource.api.DtoConverter.asDto;
+
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.media.ArraySchema;
+import io.swagger.v3.oas.annotations.media.Content;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.responses.ApiResponse;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.PathParam;
+import jakarta.ws.rs.Produces;
+import java.util.List;
+import java.util.stream.Collectors;
+import javax.inject.Inject;
+import org.eclipse.che.api.core.ConflictException;
+import org.eclipse.che.api.core.NotFoundException;
+import org.eclipse.che.api.core.ServerException;
+import org.eclipse.che.api.core.rest.Service;
+import org.eclipse.che.multiuser.resource.api.DtoConverter;
+import org.eclipse.che.multiuser.resource.shared.dto.ResourceDto;
+import org.eclipse.che.multiuser.resource.shared.dto.ResourcesDetailsDto;
+
+/**
+ * Defines Resource REST API.
+ *
+ * @author Sergii Leschenko
+ */
+@Deprecated
+@Tag(name = "resource", description = "Resource REST API")
+@Path("/resource")
+public class ResourceService extends Service {
+
+ private final ResourceManager resourceManager;
+
+ @Inject
+ public ResourceService(ResourceManager resourceManager) {
+ this.resourceManager = resourceManager;
+ }
+
+ @GET
+ @Path("/{accountId}")
+ @Produces(APPLICATION_JSON)
+ @Operation(
+ summary = "Get list of resources which are available for given account",
+ responses = {
+ @ApiResponse(
+ responseCode = "200",
+ description = "The total resources are successfully fetched",
+ content =
+ @Content(
+ array = @ArraySchema(schema = @Schema(implementation = ResourceDto.class)))),
+ @ApiResponse(responseCode = "404", description = "Account with specified id was not found"),
+ @ApiResponse(responseCode = "500", description = "Internal server error occurred")
+ })
+ public List