This is an automated email from the ASF dual-hosted git repository.

ilgrosso pushed a commit to branch 3_0_X
in repository https://gitbox.apache.org/repos/asf/syncope.git


The following commit(s) were added to refs/heads/3_0_X by this push:
     new 4b183323ac [SYNCOPE-1856]  Administrator can update and delete realms 
outside of the granted subtree (#965)
4b183323ac is described below

commit 4b183323ac38adac4c93f53c5faef2c7083c09d4
Author: Matteo Tatoni <[email protected]>
AuthorDate: Thu Jan 23 12:28:43 2025 +0100

    [SYNCOPE-1856]  Administrator can update and delete realms outside of the 
granted subtree (#965)
---
 .../syncope/client/console/panels/Realm.java       | 80 +++++++++++++---------
 .../client/console/panels/RealmChoicePanel.java    | 10 +++
 .../META-INF/resources/css/syncopeConsole.scss     |  4 ++
 .../org/apache/syncope/core/logic/RealmLogic.java  | 16 +++++
 .../org/apache/syncope/fit/core/RealmITCase.java   | 55 +++++++++++++++
 5 files changed, 132 insertions(+), 33 deletions(-)

diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/Realm.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/Realm.java
index bd82155a0d..55424fe4c5 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/Realm.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/Realm.java
@@ -24,9 +24,11 @@ import java.io.Serializable;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
+import java.util.Set;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.commons.lang3.tuple.Pair;
+import org.apache.syncope.client.console.SyncopeConsoleSession;
 import org.apache.syncope.client.console.SyncopeWebApplication;
 import org.apache.syncope.client.console.commons.ITabComponent;
 import org.apache.syncope.client.console.layout.AnyLayout;
@@ -40,7 +42,6 @@ import 
org.apache.syncope.client.console.wizards.any.ConnObjectPanel;
 import org.apache.syncope.client.ui.commons.ConnIdSpecialName;
 import org.apache.syncope.client.ui.commons.Constants;
 import org.apache.syncope.client.ui.commons.status.StatusUtils;
-import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.to.AnyTypeTO;
 import org.apache.syncope.common.lib.to.ConnObject;
 import org.apache.syncope.common.lib.to.PropagationStatus;
@@ -273,47 +274,55 @@ public abstract class Realm extends 
WizardMgtPanel<RealmTO> {
         @Override
         public Panel getPanel(final String panelId) {
             ActionsPanel<RealmTO> actionPanel = new ActionsPanel<>("actions", 
null);
+            if (securityCheck(Set.of(IdRepoEntitlement.REALM_CREATE, 
IdRepoEntitlement.REALM_UPDATE,
+                    IdRepoEntitlement.REALM_DELETE))) {
+                if (securityCheck(Set.of(IdRepoEntitlement.REALM_CREATE))) {
+                    actionPanel.add(new ActionLink<>(realmTO) {
+
+                        private static final long serialVersionUID = 
2802988981431379827L;
+
+                        @Override
+                        public void onClick(final AjaxRequestTarget target, 
final RealmTO ignore) {
+                            onClickCreate(target);
+                        }
+                    }, ActionLink.ActionType.CREATE, 
IdRepoEntitlement.REALM_CREATE).hideLabel();
+                }
 
-            if (StringUtils.startsWith(realmTO.getFullPath(), 
SyncopeConstants.ROOT_REALM)) {
-                actionPanel.add(new ActionLink<>(realmTO) {
-
-                    private static final long serialVersionUID = 
2802988981431379827L;
-
-                    @Override
-                    public void onClick(final AjaxRequestTarget target, final 
RealmTO ignore) {
-                        onClickCreate(target);
-                    }
-                }, ActionLink.ActionType.CREATE, 
IdRepoEntitlement.REALM_CREATE).hideLabel();
-
-                actionPanel.add(new ActionLink<>(realmTO) {
+                if (securityCheck(Set.of(IdRepoEntitlement.REALM_UPDATE))) {
+                    actionPanel.add(new ActionLink<>(realmTO) {
 
-                    private static final long serialVersionUID = 
2802988981431379828L;
+                        private static final long serialVersionUID = 
2802988981431379828L;
 
-                    @Override
-                    public void onClick(final AjaxRequestTarget target, final 
RealmTO ignore) {
-                        onClickEdit(target, realmTO);
-                    }
-                }, ActionLink.ActionType.EDIT, 
IdRepoEntitlement.REALM_UPDATE).hideLabel();
+                        @Override
+                        public void onClick(final AjaxRequestTarget target, 
final RealmTO ignore) {
+                            onClickEdit(target, realmTO);
+                        }
+                    }, ActionLink.ActionType.EDIT, 
IdRepoEntitlement.REALM_UPDATE).hideLabel();
+                }
 
-                actionPanel.add(new ActionLink<>(realmTO) {
+                if (securityCheck(Set.of(IdRepoEntitlement.REALM_UPDATE))) {
+                    actionPanel.add(new ActionLink<>(realmTO) {
 
-                    private static final long serialVersionUID = 
2802988981431379827L;
+                        private static final long serialVersionUID = 
2802988981431379827L;
 
-                    @Override
-                    public void onClick(final AjaxRequestTarget target, final 
RealmTO ignore) {
-                        onClickTemplate(target);
-                    }
-                }, ActionLink.ActionType.TEMPLATE, 
IdRepoEntitlement.REALM_UPDATE).hideLabel();
+                        @Override
+                        public void onClick(final AjaxRequestTarget target, 
final RealmTO ignore) {
+                            onClickTemplate(target);
+                        }
+                    }, ActionLink.ActionType.TEMPLATE, 
IdRepoEntitlement.REALM_UPDATE).hideLabel();
+                }
 
-                actionPanel.add(new ActionLink<>(realmTO) {
+                if (securityCheck(Set.of(IdRepoEntitlement.REALM_DELETE))) {
+                    actionPanel.add(new ActionLink<>(realmTO) {
 
-                    private static final long serialVersionUID = 
2802988981431379829L;
+                        private static final long serialVersionUID = 
2802988981431379829L;
 
-                    @Override
-                    public void onClick(final AjaxRequestTarget target, final 
RealmTO ignore) {
-                        onClickDelete(target, realmTO);
-                    }
-                }, ActionLink.ActionType.DELETE, 
IdRepoEntitlement.REALM_DELETE, true).hideLabel();
+                        @Override
+                        public void onClick(final AjaxRequestTarget target, 
final RealmTO ignore) {
+                            onClickDelete(target, realmTO);
+                        }
+                    }, ActionLink.ActionType.DELETE, 
IdRepoEntitlement.REALM_DELETE, true).hideLabel();
+                }
             }
 
             RealmDetails panel = new RealmDetails(panelId, realmTO, 
actionPanel, false);
@@ -327,5 +336,10 @@ public abstract class Realm extends 
WizardMgtPanel<RealmTO> {
             return 
SyncopeWebApplication.get().getSecuritySettings().getAuthorizationStrategy().
                     isActionAuthorized(this, RENDER);
         }
+
+        private boolean securityCheck(final Set<String> entitlements) {
+            return entitlements.stream()
+                    .anyMatch(entitlement -> 
SyncopeConsoleSession.get().owns(entitlement, realmTO.getFullPath()));
+        }
     }
 }
diff --git 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
index f0a50d1b09..412ec72df5 100644
--- 
a/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
+++ 
b/client/idrepo/console/src/main/java/org/apache/syncope/client/console/panels/RealmChoicePanel.java
@@ -49,6 +49,7 @@ import org.apache.syncope.common.lib.to.DynRealmTO;
 import org.apache.syncope.common.lib.to.RealmTO;
 import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.common.rest.api.beans.RealmQuery;
+import org.apache.wicket.AttributeModifier;
 import org.apache.wicket.PageReference;
 import org.apache.wicket.ajax.AjaxRequestTarget;
 import org.apache.wicket.ajax.markup.html.AjaxLink;
@@ -195,6 +196,15 @@ public class RealmChoicePanel extends Panel {
 
                     private static final long serialVersionUID = 
-817438685948164787L;
 
+                    @Override
+                    protected void onInitialize() {
+                        super.onInitialize();
+                        String fullPath = 
RealmsUtils.getFullPath(item.getModelObject());
+                        if (!SyncopeConstants.ROOT_REALM.equals(fullPath) && 
fullPath.lastIndexOf("/") == 0) {
+                            item.add(new AttributeModifier("class", 
"breadcrumb-item no-separator"));
+                        }
+                    }
+
                     @Override
                     public void onClick(final AjaxRequestTarget target) {
                         realmRestClient.search(
diff --git 
a/client/idrepo/console/src/main/resources/META-INF/resources/css/syncopeConsole.scss
 
b/client/idrepo/console/src/main/resources/META-INF/resources/css/syncopeConsole.scss
index 1deeea07ab..ed3be483af 100644
--- 
a/client/idrepo/console/src/main/resources/META-INF/resources/css/syncopeConsole.scss
+++ 
b/client/idrepo/console/src/main/resources/META-INF/resources/css/syncopeConsole.scss
@@ -116,3 +116,7 @@ body {
 .running-col {
   width: 65px;
 }
+
+/* BreadCrumb
+============================================================================= 
*/
+.no-separator::before{content:none !important;}
\ No newline at end of file
diff --git 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java
index 72917f9ee2..254f217444 100644
--- 
a/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java
+++ 
b/core/idrepo/logic/src/main/java/org/apache/syncope/core/logic/RealmLogic.java
@@ -47,6 +47,7 @@ import 
org.apache.syncope.core.persistence.api.dao.search.AnyCond;
 import org.apache.syncope.core.persistence.api.dao.search.AttrCond;
 import org.apache.syncope.core.persistence.api.dao.search.SearchCond;
 import org.apache.syncope.core.persistence.api.entity.Realm;
+import org.apache.syncope.core.persistence.api.entity.user.User;
 import org.apache.syncope.core.provisioning.api.PropagationByResource;
 import org.apache.syncope.core.provisioning.api.data.RealmDataBinder;
 import org.apache.syncope.core.provisioning.api.propagation.PropagationManager;
@@ -54,6 +55,7 @@ import 
org.apache.syncope.core.provisioning.api.propagation.PropagationReporter;
 import 
org.apache.syncope.core.provisioning.api.propagation.PropagationTaskExecutor;
 import 
org.apache.syncope.core.provisioning.api.propagation.PropagationTaskInfo;
 import org.apache.syncope.core.spring.security.AuthContextUtils;
+import 
org.apache.syncope.core.spring.security.DelegatedAdministrationException;
 import org.identityconnectors.framework.common.objects.Attribute;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.transaction.annotation.Transactional;
@@ -100,6 +102,14 @@ public class RealmLogic extends 
AbstractTransactionalLogic<RealmTO> {
         this.taskExecutor = taskExecutor;
     }
 
+    protected void securityChecks(final Set<String> effectiveRealms, final 
String realm) {
+        boolean authorized = 
effectiveRealms.stream().anyMatch(realm::startsWith);
+        if (!authorized) {
+            throw new DelegatedAdministrationException(realm, 
User.class.getSimpleName(),
+                    AuthContextUtils.getUsername());
+        }
+    }
+
     @PreAuthorize("isAuthenticated()")
     @Transactional(readOnly = true)
     public Pair<Integer, List<RealmTO>> search(
@@ -144,6 +154,8 @@ public class RealmLogic extends 
AbstractTransactionalLogic<RealmTO> {
             }
         }
 
+         
securityChecks(AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.REALM_CREATE),
 parent.getFullPath());
+
         String fullPath = StringUtils.appendIfMissing(parent.getFullPath(), 
"/") + realmTO.getName();
         if (realmDAO.findByFullPath(fullPath) != null) {
             throw new DuplicateException(fullPath);
@@ -168,6 +180,8 @@ public class RealmLogic extends 
AbstractTransactionalLogic<RealmTO> {
         Realm realm = 
Optional.ofNullable(realmDAO.findByFullPath(realmTO.getFullPath())).
                 orElseThrow(() -> new 
NotFoundException(realmTO.getFullPath()));
 
+        
securityChecks(AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.REALM_UPDATE),
 realm.getFullPath());
+
         Map<Pair<String, String>, Set<Attribute>> beforeAttrs = 
propagationManager.prepareAttrs(realm);
 
         PropagationByResource<String> propByRes = binder.update(realm, 
realmTO);
@@ -191,6 +205,8 @@ public class RealmLogic extends 
AbstractTransactionalLogic<RealmTO> {
         Realm realm = Optional.ofNullable(realmDAO.findByFullPath(fullPath)).
                 orElseThrow(() -> new NotFoundException(fullPath));
 
+        
securityChecks(AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.REALM_DELETE),
 realm.getFullPath());
+
         if (!realmDAO.findChildren(realm).isEmpty()) {
             throw 
SyncopeClientException.build(ClientExceptionType.RealmContains);
         }
diff --git 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java
index 2ee4a8605e..8a176cd606 100644
--- 
a/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java
+++ 
b/fit/core-reference/src/test/java/org/apache/syncope/fit/core/RealmITCase.java
@@ -22,6 +22,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 
@@ -30,6 +31,7 @@ import java.util.UUID;
 import javax.ws.rs.NotFoundException;
 import javax.ws.rs.core.GenericType;
 import javax.ws.rs.core.Response;
+import org.apache.syncope.client.lib.SyncopeClient;
 import org.apache.syncope.common.lib.SyncopeClientException;
 import org.apache.syncope.common.lib.SyncopeConstants;
 import org.apache.syncope.common.lib.policy.AccessPolicyTO;
@@ -40,17 +42,23 @@ import 
org.apache.syncope.common.lib.policy.DefaultAccessPolicyConf;
 import org.apache.syncope.common.lib.policy.DefaultAccountRuleConf;
 import org.apache.syncope.common.lib.policy.DefaultAttrReleasePolicyConf;
 import org.apache.syncope.common.lib.policy.DefaultAuthPolicyConf;
+import org.apache.syncope.common.lib.request.UserCR;
 import org.apache.syncope.common.lib.to.ImplementationTO;
 import org.apache.syncope.common.lib.to.PagedResult;
 import org.apache.syncope.common.lib.to.ProvisioningResult;
 import org.apache.syncope.common.lib.to.RealmTO;
+import org.apache.syncope.common.lib.to.RoleTO;
+import org.apache.syncope.common.lib.to.UserTO;
 import org.apache.syncope.common.lib.types.ClientExceptionType;
 import org.apache.syncope.common.lib.types.ExecStatus;
+import org.apache.syncope.common.lib.types.IdRepoEntitlement;
 import org.apache.syncope.common.lib.types.IdRepoImplementationType;
 import org.apache.syncope.common.lib.types.ImplementationEngine;
 import org.apache.syncope.common.lib.types.PolicyType;
 import org.apache.syncope.common.rest.api.RESTHeaders;
+import org.apache.syncope.common.rest.api.beans.AnyQuery;
 import org.apache.syncope.common.rest.api.beans.RealmQuery;
+import org.apache.syncope.common.rest.api.service.RealmService;
 import org.apache.syncope.core.provisioning.api.serialization.POJOHelper;
 import org.apache.syncope.fit.AbstractITCase;
 import org.junit.jupiter.api.Test;
@@ -401,4 +409,51 @@ public class RealmITCase extends AbstractITCase {
 
         assertFalse(realmTO.getResources().contains("resource-ldap-orgunit"), 
"Should not contain removed resources");
     }
+
+    @Test
+    public void issueSYNCOPE1856() {
+        try {
+            // CREATE ROLE
+            RoleTO roleTO = new RoleTO();
+            roleTO.getEntitlements()
+                    .addAll(List.of(IdRepoEntitlement.REALM_SEARCH, 
IdRepoEntitlement.REALM_CREATE,
+                            IdRepoEntitlement.REALM_UPDATE, 
IdRepoEntitlement.REALM_DELETE));
+            roleTO.getRealms().add("/even");
+            roleTO.setKey("REALM_ADMIN");
+            roleTO = createRole(roleTO);
+            // CREATE REALM MANAGER
+            UserCR userCR = 
UserITCase.getUniqueSample("[email protected]");
+            userCR.setUsername("manager");
+            userCR.setRealm("/even");
+            userCR.getRoles().add(roleTO.getKey());
+            UserTO manager = createUser(userCR).getEntity();
+
+            RealmService managerRealmService = 
CLIENT_FACTORY.create(manager.getUsername(), "password123")
+                    .getService(RealmService.class);
+
+            // MANAGER CANNOT CREATE REALM CHILD OF /
+            RealmTO realmTO = new RealmTO();
+            realmTO.setName("child");
+            assertThrows(SyncopeClientException.class, () -> 
managerRealmService.create("/", realmTO));
+
+            Response response = REALM_SERVICE.create("/", realmTO);
+            assertEquals(Response.Status.CREATED.getStatusCode(), 
response.getStatusInfo().getStatusCode());
+            RealmTO childRealm = REALM_SERVICE.search(new 
RealmQuery.Builder().base("/").keyword("child").build())
+                    .getResult()
+                    .get(0);
+
+            // MANAGER CANNOT UPDATE /child
+            assertThrows(SyncopeClientException.class, () -> 
managerRealmService.update(childRealm));
+
+            // MANAGER CANNOT DELETE /child
+            assertThrows(SyncopeClientException.class, () -> 
managerRealmService.delete(childRealm.getFullPath()));
+        } finally {
+            USER_SERVICE.search(new 
AnyQuery.Builder().fiql(SyncopeClient.getUserSearchConditionBuilder()
+                    .is("username")
+                    .equalTo("manager")
+                    .query()).build()).getResult().forEach(userTO -> 
deleteUser(userTO.getKey()));
+            ROLE_SERVICE.delete("REALM_ADMIN");
+            REALM_SERVICE.delete("/child");
+        }
+    }
 }

Reply via email to