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

KEYCLOAK-11494 Get users for role composite #1

Open
wants to merge 1 commit into
base: v26.0.5
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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 @@ -17,6 +17,7 @@

package org.keycloak.admin.client.resource;

import jakarta.ws.rs.DefaultValue;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.ManagementPermissionReference;
import org.keycloak.representations.idm.ManagementPermissionRepresentation;
Expand Down Expand Up @@ -131,7 +132,7 @@ public interface RoleResource {
@Path("users")
@Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> getUserMembers(@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults);
@QueryParam("max") Integer maxResults);

/**
* Get role members.
Expand All @@ -147,8 +148,8 @@ List<UserRepresentation> getUserMembers(@QueryParam("first") Integer firstResult
@Path("users")
@Produces(MediaType.APPLICATION_JSON)
List<UserRepresentation> getUserMembers(@QueryParam("briefRepresentation") Boolean briefRepresentation,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults);
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults);

/**
* Get role groups.
Expand All @@ -173,7 +174,7 @@ List<UserRepresentation> getUserMembers(@QueryParam("briefRepresentation") Boole
@Path("groups")
@Produces(MediaType.APPLICATION_JSON)
Set<GroupRepresentation> getRoleGroupMembers(@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults);
@QueryParam("max") Integer maxResults);

/**
* Get role members.
Expand Down Expand Up @@ -205,4 +206,14 @@ Set<GroupRepresentation> getRoleGroupMembers(@QueryParam("first") Integer firstR
@Deprecated
Set<UserRepresentation> getRoleUserMembers(@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults);

@GET
@Path("parents")
@Produces(MediaType.APPLICATION_JSON)
public Set<RoleRepresentation> getParentsRoles();

@GET
@Path("parents")
@Produces(MediaType.APPLICATION_JSON)
public Set<RoleRepresentation> getParentsRoles(@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation);
}
Original file line number Diff line number Diff line change
Expand Up @@ -311,13 +311,24 @@ private void roleRemovalInvalidations(String roleId, String roleName, String rol
}
}


private void invalidateRoleAndComposite(String id) {
invalidations.add(id);
RoleAdapter adapter = managedRoles.get(id);

if (adapter != null) {
adapter.invalidate();
adapter.invalidateComposites();
}
}


private void invalidateRole(String id) {
invalidations.add(id);
RoleAdapter adapter = managedRoles.get(id);
if (adapter != null) adapter.invalidate();

if (adapter != null) {
adapter.invalidate();
}
}

private void addedRole(String roleId, String roleContainerId) {
Expand Down Expand Up @@ -904,7 +915,7 @@ public RoleModel getClientRole(ClientModel client, String name) {
public boolean removeRole(RoleModel role) {
listInvalidations.add(role.getContainer().getId());

invalidateRole(role.getId());
invalidateRoleAndComposite(role.getId());
invalidationEvents.add(RoleRemovedEvent.create(role.getId(), role.getName(), role.getContainer().getId()));
roleRemovalInvalidations(role.getId(), role.getName(), role.getContainer().getId());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public class RoleAdapter implements RoleModel {
protected RealmCacheSession cacheSession;
protected RealmModel realm;
protected Set<RoleModel> composites;
protected Set<RoleModel> parents;
private final Supplier<RoleModel> modelSupplier;

public RoleAdapter(CachedRole cached, RealmCacheSession session, RealmModel realm) {
Expand All @@ -56,10 +57,18 @@ public RoleAdapter(CachedRole cached, RealmCacheSession session, RealmModel real
protected void getDelegateForUpdate() {
if (updated == null) {
cacheSession.registerRoleInvalidation(cached.getId(), cached.getName(), getContainerId());

updated = modelSupplier.get();
if (updated == null) throw new IllegalStateException("Not found in database");
}
}

protected void invalidateComposites() {
for (String roleId : cached.getComposites()) {
RoleModel role = realm.getRoleById(roleId);
cacheSession.registerRoleInvalidation(role.getId(), role.getName(), role.getContainerId());
}
}

protected boolean invalidated;

Expand Down Expand Up @@ -121,6 +130,7 @@ public void addCompositeRole(RoleModel role) {
@Override
public void removeCompositeRole(RoleModel role) {
getDelegateForUpdate();
invalidateComposites();
updated.removeCompositeRole(role);
}

Expand Down Expand Up @@ -150,6 +160,31 @@ public Stream<RoleModel> getCompositesStream(String search, Integer first, Integ

return cacheSession.getRoleDelegate().getRolesStream(realm, cached.getComposites().stream(), search, first, max);
}

@Override
public void addParentRole(RoleModel role) {
getDelegateForUpdate();
updated.addParentRole(role);
}

@Override
public Stream<RoleModel> getParentsStream() {
if (isUpdated()) return updated.getParentsStream();

if (parents == null) {
parents = new HashSet<>();
for (String id : cached.getParents()) {
RoleModel role = realm.getRoleById(id);
if (role == null) {
cacheSession.clear();
continue;
}
parents.add(role);
}
}

return parents.stream();
}

@Override
public boolean isClientRole() {
Expand Down Expand Up @@ -248,5 +283,4 @@ public boolean equals(Object o) {
public int hashCode() {
return getId().hashCode();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public class CachedRole extends AbstractRevisioned implements InRealm {
final protected String realm;
final protected String description;
final protected boolean composite;
final protected Set<String> composites = new HashSet<>();
final protected Set<String> composites = new HashSet<String>();
final protected Set<String> parents = new HashSet<String>();
private final LazyLoader<RoleModel, MultivaluedHashMap<String, String>> attributes;

public CachedRole(Long revision, RoleModel model, RealmModel realm) {
Expand All @@ -50,6 +51,10 @@ public CachedRole(Long revision, RoleModel model, RealmModel realm) {
if (composite) {
composites.addAll(model.getCompositesStream().map(RoleModel::getId).collect(Collectors.toSet()));
}

parents.addAll(model.getParentsStream().map(RoleModel::getId).collect(Collectors.toSet()));


attributes = new DefaultLazyLoader<>(roleModel -> new MultivaluedHashMap<>(roleModel.getAttributes()), MultivaluedHashMap::new);
}

Expand All @@ -72,6 +77,10 @@ public boolean isComposite() {
public Set<String> getComposites() {
return composites;
}

public Set<String> getParents() {
return parents;
}

public MultivaluedHashMap<String, String> getAttributes(Supplier<RoleModel> roleModel) {
return attributes.get(roleModel);
Expand Down
15 changes: 15 additions & 0 deletions model/jpa/src/main/java/org/keycloak/models/jpa/RoleAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,21 @@ public Stream<RoleModel> getCompositesStream(String search, Integer first, Integ
getEntity().getCompositeRoles().stream().map(RoleEntity::getId),
search, first, max);
}

@Override
public void addParentRole(RoleModel role) {
RoleEntity entity = toRoleEntity(role);
for (RoleEntity parent : getEntity().getParentRoles()) {
if (parent.equals(entity)) return;
}
getEntity().getParentRoles().add(entity);
}

@Override
public Stream<RoleModel> getParentsStream() {
Stream<RoleModel> composites = getEntity().getParentRoles().stream().map(c -> new RoleAdapter(session, realm, em, c));
return composites.filter(Objects::nonNull);
}

@Override
public boolean hasRole(RoleModel role) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ public class RoleEntity {

@ManyToMany(fetch = FetchType.LAZY, cascade = {})
@JoinTable(name = "COMPOSITE_ROLE", joinColumns = @JoinColumn(name = "COMPOSITE"), inverseJoinColumns = @JoinColumn(name = "CHILD_ROLE"))
private Set<RoleEntity> compositeRoles;
private Set<RoleEntity> compositeRoles = new HashSet<>();

@ManyToMany(fetch = FetchType.LAZY, cascade = {}, mappedBy = "compositeRoles")
private Set<RoleEntity> parentRoles = new HashSet<>();

// Explicitly not using OrphanRemoval as we're handling the removal manually through HQL but at the same time we still
// want to remove elements from the entity's collection in a manual way. Without this, Hibernate would do a duplicit
Expand Down Expand Up @@ -161,6 +164,14 @@ public Set<RoleEntity> getCompositeRoles() {
public void setCompositeRoles(Set<RoleEntity> compositeRoles) {
this.compositeRoles = compositeRoles;
}

public Set<RoleEntity> getParentRoles() {
return parentRoles;
}

public void setParentRoles(Set<RoleEntity> parentRoles) {
this.parentRoles = parentRoles;
}

public boolean isClientRole() {
return clientRole;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ public static void main(String[] args) {
// users can still provide a different folder by setting the property when starting it from their IDE.
Path path = Paths.get(System.getProperty("user.dir"), "target", "kc");
System.setProperty("kc.home.dir", path.toAbsolutePath().toString());
System.setProperty("kc.db", "mariadb");
System.setProperty("kc.db-url", "jdbc:mariadb://localhost:3306/keycloak");
System.setProperty("kc.db-username", "keycloak");
System.setProperty("kc.db-password", "keycloak");
}

if (devArgs.isEmpty()) {
Expand Down
4 changes: 4 additions & 0 deletions server-spi/src/main/java/org/keycloak/models/RoleModel.java
Original file line number Diff line number Diff line change
Expand Up @@ -100,4 +100,8 @@ default String getFirstAttribute(String name) {
Stream<String> getAttributeStream(String name);

Map<String, List<String>> getAttributes();

void addParentRole(RoleModel role);

Stream<RoleModel> getParentsStream();
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
*/
package org.keycloak.services.resources.admin;

import jakarta.ws.rs.DefaultValue;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
Expand Down Expand Up @@ -52,6 +53,7 @@
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

/**
Expand Down Expand Up @@ -245,6 +247,7 @@ public Stream<RoleRepresentation> getClientRoleComposites(final @PathParam("role
final @PathParam("clientUuid") String clientUuid) {

RoleModel role = getRoleModel(id);

auth.roles().requireView(role);
ClientModel clientModel = realm.getClientById(clientUuid);
if (clientModel == null) {
Expand Down Expand Up @@ -272,7 +275,7 @@ public void deleteComposites(final @Parameter(description = "Role id") @PathPara
}

/**
* Return object stating whether role Authorization permissions have been initialized or not and a reference
* Return object stating whether role Authoirzation permissions have been initialized or not and a reference
*
*
* @param id
Expand Down Expand Up @@ -303,6 +306,29 @@ public static ManagementPermissionReference toMgmtRef(RoleModel role, AdminPermi
return ref;
}

/**
* Get parents of the roles, thoses which have the given role as composite
*
* @param id Role id
* @param briefRepresentation if false, return a full representation of the roles with their attributes
* @return parents of the roles
*/
@Path("{role-id}/parents")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public Set<RoleRepresentation> getParentsRoles(final @PathParam("role-id") String id,
final @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation) {
RoleModel role = getRoleModel(id);
auth.roles().requireManage(role);

if (role == null) {
throw new NotFoundException("Could not find role");
}

return getParentsRoles(role, briefRepresentation);
}

/**
* Return object stating whether role Authorization permissions have been initialized or not and a reference
*
Expand Down
Loading
Loading