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");
+ }
+ }
}