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 bd82155a0d5..55424fe4c52 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.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.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 @@ protected RealmDetailsTabPanel() { @Override public Panel getPanel(final String panelId) { ActionsPanel 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 boolean isVisible() { return SyncopeWebApplication.get().getSecuritySettings().getAuthorizationStrategy(). isActionAuthorized(this, RENDER); } + + private boolean securityCheck(final Set 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 f0a50d1b09d..412ec72df52 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.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 @@ protected void populateItem(final ListItem item) { 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 1deeea07abe..ed3be483afa 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 72917f9ee29..254f2174445 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.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.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 RealmLogic( this.taskExecutor = taskExecutor; } + protected void securityChecks(final Set 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> search( @@ -144,6 +154,8 @@ public ProvisioningResult create(final String parentPath, final 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 ProvisioningResult update(final RealmTO realmTO) { Realm realm = Optional.ofNullable(realmDAO.findByFullPath(realmTO.getFullPath())). orElseThrow(() -> new NotFoundException(realmTO.getFullPath())); + securityChecks(AuthContextUtils.getAuthorizations().get(IdRepoEntitlement.REALM_UPDATE), realm.getFullPath()); + Map, Set> beforeAttrs = propagationManager.prepareAttrs(realm); PropagationByResource propByRes = binder.update(realm, realmTO); @@ -191,6 +205,8 @@ public ProvisioningResult delete(final String fullPath) { 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 2ee4a8605e3..8a176cd6069 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.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 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.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 void issueSYNCOPE1472() { 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("manager@syncope.apache.org"); + 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"); + } + } }