Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SYNCOPE-1856] Administrator can update and delete realms outside of the granted subtree #965

Merged
merged 4 commits into from
Jan 23, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,12 +42,12 @@
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;
import org.apache.syncope.common.lib.to.ProvisioningResult;
import org.apache.syncope.common.lib.to.RealmTO;
import org.apache.syncope.common.lib.to.UserTO;
import org.apache.syncope.common.lib.types.ExecStatus;
import org.apache.syncope.common.lib.types.IdRepoEntitlement;
import org.apache.wicket.Component;
Expand Down Expand Up @@ -273,47 +275,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(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(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(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(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 +337,27 @@ public boolean isVisible() {
return SyncopeWebApplication.get().getSecuritySettings().getAuthorizationStrategy().
isActionAuthorized(this, RENDER);
}

private boolean securityCheck(final String entitlement) {
ilgrosso marked this conversation as resolved.
Show resolved Hide resolved
return securityCheck(Set.of(entitlement));
}

private boolean securityCheck(final Set<String> entitlements) {
ilgrosso marked this conversation as resolved.
Show resolved Hide resolved
UserTO user = SyncopeConsoleSession.get().getSelfTO();
if (user.getUsername().equals("admin")) {
return true;
}
List<String> effectiveRealms = SyncopeConsoleSession.get()
.getSelfTO()
.getRoles()
.stream()
.map(role -> roleRestClient.read(role))
.filter(role -> role.getEntitlements().stream()
.anyMatch(entitlements::contains))
.flatMap(role -> role.getRealms().stream())
.collect(Collectors.toList());

return effectiveRealms.stream().anyMatch(realmTO.getFullPath()::startsWith);
}
}
}
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 (!fullPath.equals("/") && fullPath.lastIndexOf("/") == 0) {
ilgrosso marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -40,17 +41,22 @@
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.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 +407,49 @@ public void issueSYNCOPE1472() {

assertFalse(realmTO.getResources().contains("resource-ldap-orgunit"), "Should not contain removed resources");
}

@Test
ilgrosso marked this conversation as resolved.
Show resolved Hide resolved
public void issueSYNCOPE1856() {
// 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");
Response roleResponse = ROLE_SERVICE.create(roleTO);
ilgrosso marked this conversation as resolved.
Show resolved Hide resolved
assertEquals(Response.Status.CREATED.getStatusCode(), roleResponse.getStatusInfo().getStatusCode());

// 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()));

//CLEAN
deleteUser(manager.getKey());
ilgrosso marked this conversation as resolved.
Show resolved Hide resolved
ROLE_SERVICE.delete("REALM_ADMIN");
REALM_SERVICE.delete("/child");
}
}
Loading