mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-18 18:37:54 -05:00
Support aggregated policies during partial evaluation
Closes #45324 Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
parent
37ff64446b
commit
ab351170b4
8 changed files with 191 additions and 39 deletions
|
|
@ -18,24 +18,36 @@
|
|||
package org.keycloak.authorization.policy.provider.aggregated;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.keycloak.authorization.AuthorizationProvider;
|
||||
import org.keycloak.authorization.Decision;
|
||||
import org.keycloak.authorization.fgap.evaluation.partial.PartialEvaluationPolicyProvider;
|
||||
import org.keycloak.authorization.model.Policy;
|
||||
import org.keycloak.authorization.model.ResourceServer;
|
||||
import org.keycloak.authorization.permission.ResourcePermission;
|
||||
import org.keycloak.authorization.policy.evaluation.DecisionResultCollector;
|
||||
import org.keycloak.authorization.policy.evaluation.DefaultEvaluation;
|
||||
import org.keycloak.authorization.policy.evaluation.Evaluation;
|
||||
import org.keycloak.authorization.policy.evaluation.Result;
|
||||
import org.keycloak.authorization.policy.provider.PolicyProvider;
|
||||
import org.keycloak.authorization.store.StoreFactory;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.authorization.DecisionStrategy;
|
||||
import org.keycloak.representations.idm.authorization.ResourceType;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
|
||||
*/
|
||||
public class AggregatePolicyProvider implements PolicyProvider {
|
||||
public class AggregatePolicyProvider implements PolicyProvider, PartialEvaluationPolicyProvider {
|
||||
private static final Logger logger = Logger.getLogger(AggregatePolicyProvider.class);
|
||||
|
||||
@Override
|
||||
|
|
@ -81,4 +93,49 @@ public class AggregatePolicyProvider implements PolicyProvider {
|
|||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Policy policy) {
|
||||
return AggregatePolicyProviderFactory.ID.equals(policy.getType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<Policy> getPermissions(KeycloakSession session, ResourceType resourceType, ResourceType groupResourceType, UserModel subject) {
|
||||
AuthorizationProvider provider = session.getProvider(AuthorizationProvider.class);
|
||||
RealmModel realm = session.getContext().getRealm();
|
||||
ClientModel adminPermissionsClient = realm.getAdminPermissionsClient();
|
||||
StoreFactory storeFactory = provider.getStoreFactory();
|
||||
ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(adminPermissionsClient);
|
||||
|
||||
return storeFactory.getPolicyStore().findDependentPolicies(resourceServer, resourceType.getType(), groupResourceType == null ? null : groupResourceType.getType(), AggregatePolicyProviderFactory.ID, null, List.of());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean evaluate(KeycloakSession session, Policy policy, UserModel subject) {
|
||||
DecisionStrategy decisionStrategy = policy.getDecisionStrategy();
|
||||
Set<Policy> associatedPolicies = policy.getAssociatedPolicies();
|
||||
int grants = 0;
|
||||
|
||||
for (Policy associatedPolicy : associatedPolicies) {
|
||||
PolicyProvider policyProvider = session.getProvider(AuthorizationProvider.class).getProvider(associatedPolicy.getType());
|
||||
|
||||
if (policyProvider instanceof PartialEvaluationPolicyProvider partialPolicyProvider) {
|
||||
if (partialPolicyProvider.evaluate(session, associatedPolicy, subject)) {
|
||||
grants++;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (grants == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return switch (decisionStrategy) {
|
||||
case AFFIRMATIVE -> true;
|
||||
case UNANIMOUS -> grants == associatedPolicies.size();
|
||||
case CONSENSUS -> grants > associatedPolicies.size() - grants;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ import org.keycloak.representations.idm.authorization.PolicyRepresentation;
|
|||
*/
|
||||
public class AggregatePolicyProviderFactory implements PolicyProviderFactory<AggregatePolicyRepresentation> {
|
||||
|
||||
public static final String ID = "aggregate";
|
||||
|
||||
private AggregatePolicyProvider provider = new AggregatePolicyProvider();
|
||||
|
||||
@Override
|
||||
|
|
@ -120,6 +122,6 @@ public class AggregatePolicyProviderFactory implements PolicyProviderFactory<Agg
|
|||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "aggregate";
|
||||
return ID;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,6 +223,10 @@ make them available from queries:
|
|||
* `User`
|
||||
* `Group`
|
||||
* `Role`
|
||||
* `Aggregated`
|
||||
|
||||
In case of using an `Aggregated` policy, all the underlying policies must be of type `User`, `Group`, or `Role`. Otherwise,
|
||||
the policy will always evaluate to `DENY` when performing partial evaluation.
|
||||
|
||||
By using any of the policies above, {project_name} can pre-calculate the set of resources that a realm administration can view
|
||||
by looking for a direct (if using a user policy) or indirect (if using a role or group policy) reference to the realm administrator.
|
||||
|
|
|
|||
|
|
@ -68,7 +68,8 @@ import org.hibernate.annotations.Nationalized;
|
|||
@NamedQuery(name="findPolicyIdByResourceType", query="select p from PolicyEntity p inner join p.config c inner join fetch p.associatedPolicies a where p.resourceServer.id = :serverId and KEY(c) = 'defaultResourceType' and c like :type"),
|
||||
@NamedQuery(name="findPolicyIdByDependentPolices", query="select p.id from PolicyEntity p inner join p.associatedPolicies ap where p.resourceServer.id = :serverId and (ap.resourceServer.id = :serverId and ap.id = :policyId)"),
|
||||
@NamedQuery(name="deletePolicyByResourceServer", query="delete from PolicyEntity p where p.resourceServer.id = :serverId"),
|
||||
@NamedQuery(name="findDependentPolicyByResourceTypeAndConfig", query="select p.id from PolicyEntity p inner join p.scopes s inner join p.config c inner join p.associatedPolicies ap inner join ap.config ac where p.resourceServer.id = :serverId and (s.name = :scopeName) and ap.resourceServer.id = :serverId and ap.type = :associatedPolicyType and (KEY(c) = 'defaultResourceType' and c like :resourceType) and (KEY(ac) = :configKey and ac like :configValue)")
|
||||
@NamedQuery(name="findDependentPolicyByResourceTypeAndConfig", query="select p.id from PolicyEntity p inner join p.scopes s inner join p.config c inner join p.associatedPolicies ap inner join ap.config ac where p.resourceServer.id = :serverId and (s.name = :scopeName) and ap.resourceServer.id = :serverId and ap.type = :associatedPolicyType and (KEY(c) = 'defaultResourceType' and c like :resourceType) and (KEY(ac) = :configKey and ac like :configValue)"),
|
||||
@NamedQuery(name="findDependentPolicyByResourceType", query="select p.id from PolicyEntity p inner join p.scopes s inner join p.config c inner join p.associatedPolicies ap where p.resourceServer.id = :serverId and (s.name = :scopeName) and ap.resourceServer.id = :serverId and ap.type = :associatedPolicyType and (KEY(c) = 'defaultResourceType' and c like :resourceType)")
|
||||
}
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -339,29 +339,41 @@ public class JPAPolicyStore implements PolicyStore {
|
|||
String dbProductName = entityManager.unwrap(Session.class).doReturningWork(connection -> connection.getMetaData().getDatabaseProductName());
|
||||
|
||||
if (dbProductName.equals("Oracle")) {
|
||||
Stream<Policy> result = Stream.empty();
|
||||
TypedQuery<String> query;
|
||||
|
||||
for (String value : configValues) {
|
||||
TypedQuery<String> query = entityManager.createNamedQuery("findDependentPolicyByResourceTypeAndConfig", String.class);
|
||||
|
||||
query.setParameter("serverId", resourceServer.getId());
|
||||
query.setParameter("resourceType", resourceType);
|
||||
query.setParameter("associatedPolicyType", associatedPolicyType);
|
||||
query.setParameter("configKey", configKey);
|
||||
query.setParameter("configValue", "%" + value + "%");
|
||||
|
||||
if (AdminPermissionsSchema.GROUPS.getType().equals(groupResourceType)) {
|
||||
query.setParameter("scopeName", AdminPermissionsSchema.VIEW_MEMBERS);
|
||||
} else {
|
||||
query.setParameter("scopeName", AdminPermissionsSchema.VIEW);
|
||||
}
|
||||
|
||||
PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
|
||||
|
||||
result = Stream.concat(result, query.getResultStream().map((id) -> policyStore.findById(resourceServer, id)).filter(Objects::nonNull));
|
||||
if (configKey == null) {
|
||||
query = entityManager.createNamedQuery("findDependentPolicyByResourceType", String.class);
|
||||
} else {
|
||||
query = entityManager.createNamedQuery("findDependentPolicyByResourceTypeAndConfig", String.class);
|
||||
}
|
||||
|
||||
return result;
|
||||
query.setParameter("serverId", resourceServer.getId());
|
||||
query.setParameter("resourceType", resourceType);
|
||||
query.setParameter("associatedPolicyType", associatedPolicyType);
|
||||
|
||||
if (AdminPermissionsSchema.GROUPS.getType().equals(groupResourceType)) {
|
||||
query.setParameter("scopeName", AdminPermissionsSchema.VIEW_MEMBERS);
|
||||
} else {
|
||||
query.setParameter("scopeName", AdminPermissionsSchema.VIEW);
|
||||
}
|
||||
|
||||
if (configKey == null) {
|
||||
PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
|
||||
return query.getResultStream().map((id) -> policyStore.findById(resourceServer, id)).filter(Objects::nonNull);
|
||||
} else {
|
||||
Stream<Policy> result = Stream.empty();
|
||||
|
||||
for (String value : configValues) {
|
||||
query.setParameter("configKey", configKey);
|
||||
query.setParameter("configValue", "%" + value + "%");
|
||||
|
||||
PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
|
||||
|
||||
result = Stream.concat(result, query.getResultStream().map((id) -> policyStore.findById(resourceServer, id)).filter(Objects::nonNull));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
|
||||
|
|
@ -373,7 +385,6 @@ public class JPAPolicyStore implements PolicyStore {
|
|||
Join<Object, Object> scope = from.join("scopes");
|
||||
MapJoin<Object, Object, Object> config = from.joinMap("config");
|
||||
Join<Object, Object> associatedPolicy = from.join("associatedPolicies");
|
||||
MapJoin<Object, Object, Object> associatedPolicyConfig = associatedPolicy.joinMap("config");
|
||||
|
||||
List<Predicate> predicates = new LinkedList<>();
|
||||
|
||||
|
|
@ -389,16 +400,19 @@ public class JPAPolicyStore implements PolicyStore {
|
|||
predicates.add(cb.equal(config.key(), "defaultResourceType"));
|
||||
predicates.add(cb.equal(config.value(), resourceType));
|
||||
|
||||
List<Predicate> configValuePredicates = new LinkedList<>();
|
||||
if (configKey != null) {
|
||||
MapJoin<Object, Object, Object> associatedPolicyConfig = associatedPolicy.joinMap("config");
|
||||
List<Predicate> configValuePredicates = new LinkedList<>();
|
||||
|
||||
predicates.add(cb.equal(associatedPolicyConfig.key(), configKey));
|
||||
predicates.add(cb.equal(associatedPolicyConfig.key(), configKey));
|
||||
|
||||
for (String value : configValues) {
|
||||
configValuePredicates.add(cb.like(associatedPolicyConfig.value().as(String.class), "%" + value + "%"));
|
||||
for (String value : configValues) {
|
||||
configValuePredicates.add(cb.like(associatedPolicyConfig.value().as(String.class), "%" + value + "%"));
|
||||
}
|
||||
|
||||
predicates.add(cb.or(configValuePredicates.toArray(new Predicate[0])));
|
||||
}
|
||||
|
||||
predicates.add(cb.or(configValuePredicates.toArray(new Predicate[0])));
|
||||
|
||||
query.where(predicates.toArray(new Predicate[0]));
|
||||
|
||||
PolicyStore policyStore = provider.getStoreFactory().getPolicyStore();
|
||||
|
|
|
|||
|
|
@ -31,8 +31,11 @@ import org.keycloak.authorization.fgap.AdminPermissionsSchema;
|
|||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.AggregatePolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.ClientPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.DecisionStrategy;
|
||||
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.Logic;
|
||||
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
|
||||
|
|
@ -43,6 +46,8 @@ import org.keycloak.testframework.annotations.InjectRealm;
|
|||
import org.keycloak.testframework.injection.LifeCycle;
|
||||
import org.keycloak.testframework.realm.ManagedClient;
|
||||
import org.keycloak.testframework.realm.ManagedRealm;
|
||||
import org.keycloak.testframework.realm.UserConfigBuilder;
|
||||
import org.keycloak.testframework.util.ApiUtil;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.equalTo;
|
||||
|
|
@ -193,6 +198,17 @@ public abstract class AbstractPermissionTest {
|
|||
return policy;
|
||||
}
|
||||
|
||||
protected AggregatePolicyRepresentation createAggregatedPolicy(ManagedClient client, String name, Logic logic, DecisionStrategy decisionStrategy, String... policies) {
|
||||
AggregatePolicyRepresentation aggregatedPolicy = new AggregatePolicyRepresentation();
|
||||
aggregatedPolicy.setName(name);
|
||||
aggregatedPolicy.setLogic(logic);
|
||||
aggregatedPolicy.setDecisionStrategy(decisionStrategy);
|
||||
aggregatedPolicy.setPolicies(Set.of(policies));
|
||||
try (Response response = client.admin().authorization().policies().aggregate().create(aggregatedPolicy)) {
|
||||
return response.readEntity(AggregatePolicyRepresentation.class);
|
||||
}
|
||||
}
|
||||
|
||||
protected ScopePermissionRepresentation createAllPermission(ManagedClient client, String resourceType, AbstractPolicyRepresentation policy, Set<String> scopes) {
|
||||
ScopePermissionRepresentation permission = PermissionBuilder.create()
|
||||
.resourceType(resourceType)
|
||||
|
|
@ -228,4 +244,26 @@ public abstract class AbstractPermissionTest {
|
|||
protected ScopePermissionRepresentation createGroupPermission(Set<GroupRepresentation> groups, Set<String> scopes, AbstractPolicyRepresentation... policies) {
|
||||
return createPermission(client, groups.stream().map(GroupRepresentation::getId).collect(Collectors.toSet()), AdminPermissionsSchema.GROUPS_RESOURCE_TYPE, scopes, policies);
|
||||
}
|
||||
|
||||
protected UserRepresentation createUser(String username) {
|
||||
UserRepresentation user = UserConfigBuilder.create()
|
||||
.username(username)
|
||||
.build();
|
||||
|
||||
try (Response response = realm.admin().users().create(user)) {
|
||||
user.setId(ApiUtil.getCreatedId(response));
|
||||
return user;
|
||||
}
|
||||
}
|
||||
|
||||
protected GroupRepresentation createGroup(String name) {
|
||||
GroupRepresentation group = new GroupRepresentation();
|
||||
|
||||
group.setName(name);
|
||||
|
||||
try (Response response = realm.admin().groups().add(group)) {
|
||||
group.setId(ApiUtil.getCreatedId(response));
|
||||
return group;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -261,13 +261,4 @@ public class FineGrainedPermissionsUsersTest extends AbstractPermissionTest {
|
|||
|
||||
return groups;
|
||||
}
|
||||
|
||||
private GroupRepresentation createGroup(String name) {
|
||||
GroupRepresentation grp = new GroupRepresentation();
|
||||
grp.setName(name);
|
||||
String groupId = ApiUtil.getCreatedId(realm.admin().groups().add(grp));
|
||||
grp.setId(groupId);
|
||||
realm.cleanup().add(r -> r.groups().group(groupId).remove());
|
||||
return grp;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ import org.keycloak.models.utils.KeycloakModelUtils;
|
|||
import org.keycloak.representations.idm.GroupRepresentation;
|
||||
import org.keycloak.representations.idm.RoleRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.AggregatePolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.DecisionStrategy;
|
||||
import org.keycloak.representations.idm.authorization.GroupPolicyRepresentation;
|
||||
import org.keycloak.representations.idm.authorization.Logic;
|
||||
import org.keycloak.representations.idm.authorization.RolePolicyRepresentation;
|
||||
|
|
@ -61,6 +63,8 @@ import org.junit.jupiter.api.BeforeEach;
|
|||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.GROUPS_RESOURCE_TYPE;
|
||||
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.MANAGE_MEMBERS;
|
||||
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.MANAGE_MEMBERSHIP;
|
||||
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.USERS_RESOURCE_TYPE;
|
||||
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.VIEW;
|
||||
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.VIEW_MEMBERS;
|
||||
|
|
@ -527,4 +531,45 @@ public class UserResourceTypeFilteringTest extends AbstractPermissionTest {
|
|||
assertThat(roleMembers, hasSize(allowedUsers.size()));
|
||||
assertThat(roleMembers, hasItems(allowedUsers.toArray(new String[0])));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testViewGroupMembersPolicyUsingAggregatedPolicy() {
|
||||
List<UserRepresentation> search = realmAdminClient.realm(realm.getName()).users().search(null, 0, 10);
|
||||
assertTrue(search.isEmpty());
|
||||
|
||||
GroupRepresentation fooGroup = createGroup(KeycloakModelUtils.generateId());
|
||||
UserRepresentation fooUser = createUser(KeycloakModelUtils.generateId());
|
||||
realm.admin().users().get(fooUser.getId()).joinGroup(fooGroup.getId());
|
||||
GroupRepresentation fooGroupManager = createGroup(KeycloakModelUtils.generateId());
|
||||
|
||||
UserRepresentation barUser = createUser(KeycloakModelUtils.generateId());
|
||||
GroupRepresentation barGroup = createGroup(KeycloakModelUtils.generateId());
|
||||
realm.admin().users().get(barUser.getId()).joinGroup(barGroup.getId());
|
||||
GroupRepresentation barGroupManager = createGroup(KeycloakModelUtils.generateId());
|
||||
|
||||
GroupPolicyRepresentation fooGroupManagerPolicy = createGroupPolicy(realm, client, "Foo Group Policy", Logic.POSITIVE, fooGroupManager.getId());
|
||||
GroupPolicyRepresentation barGroupManagerPolicy = createGroupPolicy(realm, client, "Bar Group Policy", Logic.POSITIVE, barGroupManager.getId());
|
||||
AggregatePolicyRepresentation aggregatedPolicy = createAggregatedPolicy(client, "Foo and Bar Group Policy", Logic.POSITIVE, DecisionStrategy.AFFIRMATIVE, fooGroupManagerPolicy.getName(), barGroupManagerPolicy.getName());
|
||||
|
||||
search = realmAdminClient.realm(realm.getName()).users().search(null, 0, 10);
|
||||
assertTrue(search.isEmpty());
|
||||
|
||||
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
|
||||
createAllPermission(client, GROUPS_RESOURCE_TYPE, aggregatedPolicy, Set.of(VIEW_MEMBERS, MANAGE_MEMBERSHIP, MANAGE_MEMBERS));
|
||||
|
||||
realm.admin().users().get(myadmin.getId()).joinGroup(fooGroupManager.getId());
|
||||
search = realmAdminClient.realm(realm.getName()).users().search(null, 0, 10);
|
||||
assertEquals(3, search.size());
|
||||
assertTrue(search.stream().map(UserRepresentation::getUsername).anyMatch(fooUser.getUsername()::equals));
|
||||
assertTrue(search.stream().map(UserRepresentation::getUsername).anyMatch(barUser.getUsername()::equals));
|
||||
|
||||
aggregatedPolicy.setDecisionStrategy(DecisionStrategy.UNANIMOUS);
|
||||
client.admin().authorization().policies().aggregate().findById(aggregatedPolicy.getId()).update(aggregatedPolicy);
|
||||
search = realmAdminClient.realm(realm.getName()).users().search(null, 0, 10);
|
||||
assertTrue(search.isEmpty());
|
||||
|
||||
realm.admin().users().get(myadmin.getId()).joinGroup(barGroupManager.getId());
|
||||
search = realmAdminClient.realm(realm.getName()).users().search(null, 0, 10);
|
||||
assertEquals(3, search.size());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue