Skip to content

Commit

Permalink
[SYNCOPE-1856] Administrator can update and delete realms outside of …
Browse files Browse the repository at this point in the history
…the granted subtree (#965)
  • Loading branch information
TatoniMatteo authored Jan 23, 2025
1 parent fb91462 commit 4b18332
Show file tree
Hide file tree
Showing 5 changed files with 132 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -273,47 +274,55 @@ protected RealmDetailsTabPanel() {
@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);
Expand All @@ -327,5 +336,10 @@ public boolean isVisible() {
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()));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -195,6 +196,15 @@ protected void populateItem(final ListItem<String> 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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,3 +116,7 @@ body {
.running-col {
width: 65px;
}

/* BreadCrumb
============================================================================= */
.no-separator::before{content:none !important;}
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,15 @@
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;
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;
Expand Down Expand Up @@ -100,6 +102,14 @@ public RealmLogic(
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(
Expand Down Expand Up @@ -144,6 +154,8 @@ public ProvisioningResult<RealmTO> 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);
Expand All @@ -168,6 +180,8 @@ public ProvisioningResult<RealmTO> 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<Pair<String, String>, Set<Attribute>> beforeAttrs = propagationManager.prepareAttrs(realm);

PropagationByResource<String> propByRes = binder.update(realm, realmTO);
Expand All @@ -191,6 +205,8 @@ public ProvisioningResult<RealmTO> 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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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("[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");
}
}
}

0 comments on commit 4b18332

Please sign in to comment.