Introduce ORGANIZATIONS resource type in Fine-Grained Admin Permissions

Closes #47284

Signed-off-by: vramik <vramik@redhat.com>
This commit is contained in:
vramik 2026-04-21 13:18:19 +02:00 committed by Pedro Igor
parent bc11757f22
commit e1329516d5
28 changed files with 899 additions and 85 deletions

View file

@ -5,11 +5,11 @@ This release features new capabilities for users and administrators of {project_
= Administration
== Dedicated admin roles for organization management
== Delegated administration for organizations
{project_name} now provides dedicated realm admin roles for managing organizations: `manage-organizations`, `view-organizations`, and `query-organizations`.
{project_name} now supports delegated organization administration without requiring the broad `manage-realm` role. This is achieved through new dedicated admin roles and Fine-Grained Admin Permissions support for organizations.
Previously, managing organizations required the `manage-realm` role, which grants broad administrative access. The new roles allow administrators to be granted organization-specific permissions without requiring full realm management privileges.
New realm admin roles provide coarse-grained delegation:
* `manage-organizations` — grants full read and write access to organizations, including creating, updating, and deleting organizations and their members.
* `view-organizations` — grants read-only access to organizations and their members (also requires `view-users` or Fine-Grained Admin Permissions for user visibility).
@ -17,7 +17,7 @@ Previously, managing organizations required the `manage-realm` role, which grant
The `manage-realm` role continues to implicitly grant full organization management access for backward compatibility.
When Fine-Grained Admin Permissions is enabled, organization member queries respect user-level permissions, returning only members the administrator is permitted to view.
For per-organization granularity, organizations are now a first-class resource type in Fine-Grained Admin Permissions. Administrators can create permissions to control which specific organizations a delegated administrator can view or manage — for example, granting access to manage one organization without giving access to all organizations in the realm. When Fine-Grained Admin Permissions is enabled, organization member queries also respect user-level permissions, returning only members the administrator is permitted to view.
== Realm search now matches by display name

View file

@ -52,6 +52,7 @@ This feature provides the necessary mechanisms to enforce access controls when m
* Groups
* Clients
* Roles
* Organizations
You can manage permissions for all resources of a given resource type, such as all users in a realm, or
for a specific realm resource, such as a specific user or set of users in the realm.
@ -173,6 +174,22 @@ have user role mapping permissions on the user.
If there is a client resource type permission for the *map-roles*, *map-roles-composite*, or *map-roles-client-scope* scopes,
it will take precedence over any role resource type permission if the role is a client role.
===== Organizations Resource Type
The *Organizations* realm resource type represents the organizations in a realm. You can manage permissions for organizations based on the following
set of management operations:
[cols="30%,70%"]
|===
| *Operation* | *Description*
| *view* | Defines if a realm administrator can view organizations. This scope should be set whenever you want
to make organizations available from queries.
| *manage* | Defines if a realm administrator can manage organizations, including updating and deleting them.
|===
Creating new organizations requires `manage` permission on all organizations (the resource type level), not just on a specific organization.
==== Enabling admin permissions to a realm
To enable fine-grained admin permissions in a realm, follow these steps:
@ -387,6 +404,8 @@ if a respective admin role is assigned to a realm administrator, permission eval
| *impersonation* | A realm administrator can *impersonate* all users in the realm.
| *view-clients* | A realm administrator can *view* all clients in the realm.
| *manage-clients* | A realm administrator can *view* and *manage* all clients and client scopes in the realm.
| *view-organizations* | A realm administrator can *view* all organizations in the realm.
| *manage-organizations* | A realm administrator can *view* and *manage* all organizations in the realm.
|===
When this feature is enabled in a realm, only server and realm administrators with the corresponding admin roles can grant these roles

View file

@ -22,6 +22,7 @@ import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import org.keycloak.authorization.fgap.AdminPermissionsSchema;
import org.keycloak.models.GroupModel;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
@ -260,6 +261,10 @@ public class InfinispanOrganizationProvider implements OrganizationProvider {
@Override
public Stream<OrganizationModel> getByMember(UserModel member) {
if (AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(getRealm())) {
return getCacheDelegates(getDelegate().getByMember(member));
}
if (userCache == null) {
return getDelegate().getByMember(member);
}

View file

@ -43,9 +43,7 @@ import org.keycloak.utils.StringUtil;
@NamedQuery(name="getByDomainName", query="select distinct o from OrganizationEntity o inner join OrganizationDomainEntity d ON o.id = d.organization.id" +
" where o.realmId = :realmId AND d.name = :name"),
@NamedQuery(name="getCount", query="select count(o) from OrganizationEntity o where o.realmId = :realmId"),
@NamedQuery(name="deleteOrganizationsByRealm", query="delete from OrganizationEntity o where o.realmId = :realmId"),
@NamedQuery(name="getInternalOrgGroupsByMember", query="select m.groupId from UserGroupMembershipEntity m join GroupEntity g on g.id = m.groupId where g.type = 1 and m.user.id = :userId"),
@NamedQuery(name="getInternalOrgGroupsByFederatedMember", query="select m.groupId from FederatedUserGroupMembershipEntity m join GroupEntity g on g.id = m.groupId where g.type = 1 and m.userId = :userId")
@NamedQuery(name="deleteOrganizationsByRealm", query="delete from OrganizationEntity o where o.realmId = :realmId")
})
public class OrganizationEntity {

View file

@ -167,6 +167,8 @@ public class JpaOrganizationProvider implements OrganizationProvider {
organization.getIdentityProviders().forEach((model) -> removeIdentityProvider(organization, model));
}
OrganizationModel.OrganizationRemovedEvent.fire(organization, session);
em.remove(entity);
} finally {
session.getContext().setOrganization(null);
@ -251,9 +253,12 @@ public class JpaOrganizationProvider implements OrganizationProvider {
CriteriaQuery<OrganizationEntity> query = builder.createQuery(OrganizationEntity.class);
Root<OrganizationEntity> org = query.from(OrganizationEntity.class);
Predicate predicate = buildStringSearchPredicate(builder, query, org, search, exact);
List<Predicate> predicates = new ArrayList<>();
predicates.add(buildStringSearchPredicate(builder, query, org, search, exact));
predicates.addAll(AdminPermissionsSchema.SCHEMA.applyAuthorizationFilters(
session, AdminPermissionsSchema.ORGANIZATIONS, getRealm(), builder, query, org));
TypedQuery<OrganizationEntity> typedQuery = buildSearchQuery(builder, query, org, predicate);
TypedQuery<OrganizationEntity> typedQuery = buildSearchQuery(builder, query, org, predicates);
return closing(paginateQuery(typedQuery, first, max).getResultStream()
.map(entity -> new OrganizationAdapter(session, getRealm(), entity, this)));
@ -265,10 +270,12 @@ public class JpaOrganizationProvider implements OrganizationProvider {
CriteriaQuery<OrganizationEntity> query = builder.createQuery(OrganizationEntity.class);
Root<OrganizationEntity> org = query.from(OrganizationEntity.class);
Predicate predicate = buildAttributeSearchPredicate(builder, query, org, attributes);
List<Predicate> predicates = new ArrayList<>();
predicates.add(buildAttributeSearchPredicate(builder, query, org, attributes));
predicates.addAll(AdminPermissionsSchema.SCHEMA.applyAuthorizationFilters(
session, AdminPermissionsSchema.ORGANIZATIONS, getRealm(), builder, query, org));
TypedQuery<OrganizationEntity> typedQuery = buildSearchQuery(builder, query, org, predicate);
TypedQuery<OrganizationEntity> typedQuery = buildSearchQuery(builder, query, org, predicates);
return closing(paginateQuery(typedQuery, first, max).getResultStream())
.map(entity -> new OrganizationAdapter(session, getRealm(), entity, this));
}
@ -279,9 +286,12 @@ public class JpaOrganizationProvider implements OrganizationProvider {
CriteriaQuery<Long> query = builder.createQuery(Long.class);
Root<OrganizationEntity> org = query.from(OrganizationEntity.class);
Predicate predicate = buildStringSearchPredicate(builder, query, org, search, exact);
List<Predicate> predicates = new ArrayList<>();
predicates.add(buildStringSearchPredicate(builder, query, org, search, exact));
predicates.addAll(AdminPermissionsSchema.SCHEMA.applyAuthorizationFilters(
session, AdminPermissionsSchema.ORGANIZATIONS, getRealm(), builder, query, org));
TypedQuery<Long> typedQuery = buildCountQuery(builder, query, org, predicate);
TypedQuery<Long> typedQuery = buildCountQuery(builder, query, org, predicates);
return typedQuery.getSingleResult();
}
@ -292,10 +302,12 @@ public class JpaOrganizationProvider implements OrganizationProvider {
CriteriaQuery<Long> query = builder.createQuery(Long.class);
Root<OrganizationEntity> org = query.from(OrganizationEntity.class);
Predicate predicate = buildAttributeSearchPredicate(builder, query, org, attributes);
List<Predicate> predicates = new ArrayList<>();
predicates.add(buildAttributeSearchPredicate(builder, query, org, attributes));
predicates.addAll(AdminPermissionsSchema.SCHEMA.applyAuthorizationFilters(
session, AdminPermissionsSchema.ORGANIZATIONS, getRealm(), builder, query, org));
TypedQuery<Long> typedQuery = buildCountQuery(builder, query, org, predicate);
TypedQuery<Long> typedQuery = buildCountQuery(builder, query, org, predicates);
return typedQuery.getSingleResult();
}
@ -303,14 +315,14 @@ public class JpaOrganizationProvider implements OrganizationProvider {
private TypedQuery<OrganizationEntity> buildSearchQuery(CriteriaBuilder builder,
CriteriaQuery<OrganizationEntity> query,
Root<OrganizationEntity> org,
Predicate predicate) {
List<Predicate> predicates) {
return em.createQuery(
query.select(org).distinct(true).where(predicate).orderBy(builder.asc(org.get("name"))));
query.select(org).distinct(true).where(predicates.toArray(Predicate[]::new)).orderBy(builder.asc(org.get("name"))));
}
private TypedQuery<Long> buildCountQuery(CriteriaBuilder builder, CriteriaQuery<Long> query,
Root<OrganizationEntity> org, Predicate predicate) {
return em.createQuery(query.select(builder.countDistinct(org)).where(predicate));
Root<OrganizationEntity> org, List<Predicate> predicates) {
return em.createQuery(query.select(builder.countDistinct(org)).where(predicates.toArray(Predicate[]::new)));
}
private Predicate buildStringSearchPredicate(CriteriaBuilder builder, CriteriaQuery<?> query, Root<OrganizationEntity> org, String search,
@ -488,22 +500,29 @@ public class JpaOrganizationProvider implements OrganizationProvider {
public Stream<OrganizationModel> getByMember(UserModel member) {
throwExceptionIfObjectIsNull(member, "User");
TypedQuery<String> query;
if(StorageId.isLocalStorage(member.getId())) {
query = em.createNamedQuery("getInternalOrgGroupsByMember", String.class);
CriteriaBuilder builder = em.getCriteriaBuilder();
CriteriaQuery<OrganizationEntity> query = builder.createQuery(OrganizationEntity.class);
Root<OrganizationEntity> org = query.from(OrganizationEntity.class);
List<Predicate> predicates = new ArrayList<>();
if (StorageId.isLocalStorage(member.getId())) {
Root<UserGroupMembershipEntity> membership = query.from(UserGroupMembershipEntity.class);
predicates.add(builder.equal(org.get("groupId"), membership.get("groupId")));
predicates.add(builder.equal(membership.get("user").get("id"), member.getId()));
} else {
query = em.createNamedQuery("getInternalOrgGroupsByFederatedMember", String.class);
Root<FederatedUserGroupMembershipEntity> membership = query.from(FederatedUserGroupMembershipEntity.class);
predicates.add(builder.equal(org.get("groupId"), membership.get("groupId")));
predicates.add(builder.equal(membership.get("userId"), member.getId()));
}
query.setParameter("userId", member.getId());
predicates.addAll(AdminPermissionsSchema.SCHEMA.applyAuthorizationFilters(
session, AdminPermissionsSchema.ORGANIZATIONS, getRealm(), builder, query, org));
OrganizationProvider organizations = session.getProvider(OrganizationProvider.class);
GroupProvider groups = session.groups();
TypedQuery<OrganizationEntity> typedQuery = buildSearchQuery(builder, query, org, predicates);
return closing(query.getResultStream())
.map((id) -> groups.getGroupById(getRealm(), id))
.map((g) -> organizations.getById(g.getName()))
.filter(Objects::nonNull);
return closing(typedQuery.getResultStream()
.map(entity -> new OrganizationAdapter(session, getRealm(), entity, this)));
}
@Override

View file

@ -6,6 +6,7 @@ import java.util.List;
import java.util.Map;
import org.keycloak.Config;
import org.keycloak.authorization.fgap.AdminPermissionsSchema;
import org.keycloak.migration.ModelVersion;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.ClientModel;
@ -46,6 +47,7 @@ public class MigrateTo26_7_0 extends RealmMigration {
@Override
public void migrateRealm(KeycloakSession session, RealmModel realm) {
updatePasswordAfterEmailVerificationDuringRegistrationOfUsers(realm);
updateAdminPermissionsSchema(session, realm);
}
private void updatePasswordAfterEmailVerificationDuringRegistrationOfUsers(RealmModel realm) {
@ -116,4 +118,10 @@ public class MigrateTo26_7_0 extends RealmMigration {
viewOrganizations.addCompositeRole(queryOrganizations);
}
}
private void updateAdminPermissionsSchema(KeycloakSession session, RealmModel realm) {
if (realm.getAdminPermissionsClient() != null) {
AdminPermissionsSchema.SCHEMA.init(session, realm);
}
}
}

View file

@ -53,6 +53,7 @@ import org.keycloak.models.GroupModel.GroupRemovedEvent;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.ModelValidationException;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleContainerModel.RoleRemovedEvent;
import org.keycloak.models.RoleModel;
@ -60,6 +61,7 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.UserModel.UserRemovedEvent;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.provider.ProviderEvent;
import org.keycloak.representations.idm.authorization.AbstractPolicyRepresentation;
import org.keycloak.representations.idm.authorization.AuthorizationSchema;
@ -77,6 +79,7 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
public static final String GROUPS_RESOURCE_TYPE = "Groups";
public static final String ROLES_RESOURCE_TYPE = "Roles";
public static final String USERS_RESOURCE_TYPE = "Users";
public static final String ORGANIZATIONS_RESOURCE_TYPE = "Organizations";
// common scopes
public static final String MANAGE = "manage";
@ -110,6 +113,7 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
public static final ResourceType GROUPS = new ResourceType(GROUPS_RESOURCE_TYPE, Set.of(MANAGE, VIEW, MANAGE_MEMBERSHIP, MANAGE_MEMBERSHIP_OF_MEMBERS, MANAGE_MEMBERS, VIEW_MEMBERS, IMPERSONATE_MEMBERS));
public static final ResourceType ROLES = new ResourceType(ROLES_RESOURCE_TYPE, Set.of(MAP_ROLE, MAP_ROLE_CLIENT_SCOPE, MAP_ROLE_COMPOSITE));
public static final ResourceType USERS = new ResourceType(USERS_RESOURCE_TYPE, Set.of(MANAGE, VIEW, IMPERSONATE, MAP_ROLES, MANAGE_GROUP_MEMBERSHIP, RESET_PASSWORD), Map.of(VIEW, Set.of(VIEW_MEMBERS), MANAGE, Set.of(MANAGE_MEMBERS), IMPERSONATE, Set.of(IMPERSONATE_MEMBERS), MANAGE_GROUP_MEMBERSHIP, Set.of(MANAGE_MEMBERSHIP_OF_MEMBERS)), GROUPS.getType());
public static final ResourceType ORGANIZATIONS = new ResourceType(ORGANIZATIONS_RESOURCE_TYPE, Set.of(MANAGE, VIEW));
private static final String SKIP_EVALUATION = "kc.authz.fgap.skip";
public static final AdminPermissionsSchema SCHEMA = new AdminPermissionsSchema();
@ -121,7 +125,8 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
CLIENTS_RESOURCE_TYPE, CLIENTS,
GROUPS_RESOURCE_TYPE, GROUPS,
ROLES_RESOURCE_TYPE, ROLES,
USERS_RESOURCE_TYPE, USERS
USERS_RESOURCE_TYPE, USERS,
ORGANIZATIONS_RESOURCE_TYPE, ORGANIZATIONS
));
}
@ -172,6 +177,8 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
.orElseThrow(() -> new ModelValidationException("Resource [" + id + "] does not exist for type [" + resourceType + "]"));
case USERS_RESOURCE_TYPE -> resolveUser(session, id).map(UserModel::getId)
.orElseThrow(() -> new ModelValidationException("Resource [" + id + "] does not exist for type [" + resourceType + "]"));
case ORGANIZATIONS_RESOURCE_TYPE -> resolveOrganization(session, id).map(OrganizationModel::getId)
.orElseThrow(() -> new ModelValidationException("Resource [" + id + "] does not exist for type [" + resourceType + "]"));
default -> throw new IllegalStateException("Resource type [" + resourceType + "] not found.");
};
}
@ -248,6 +255,10 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
return Optional.ofNullable(user);
}
private Optional<OrganizationModel> resolveOrganization(KeycloakSession session, String id) {
return Optional.ofNullable(session.getProvider(OrganizationProvider.class).getById(id));
}
private Optional<ClientModel> resolveClient(KeycloakSession session, String id) {
RealmModel realm = session.getContext().getRealm();
ClientModel client = session.clients().getClientById(realm, id);
@ -300,6 +311,7 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
ClientModel client = realm.getAdminPermissionsClient();
if (client != null) {
ensureSchemaUpToDate(session, client);
return;
}
@ -312,7 +324,7 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
ResourceServerRepresentation resourceServerRep = ModelToRepresentation.toRepresentation(resourceServer, client);
//create all scopes defined in the schema
//there is no way how to map scopes to the resourceType, we need to collect all scopes from all resourceTypes
//there is no way how to map scopes to the resourceType, we need to collect all scopes from all resourceTypes
Set<ScopeRepresentation> scopes = SCHEMA.getResourceTypes().values().stream()
.flatMap((resourceType) -> resourceType.getScopes().stream())
.map(ScopeRepresentation::new)
@ -331,6 +343,39 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
RepresentationToModel.toModel(resourceServerRep, session.getProvider(AuthorizationProvider.class), client);
}
private void ensureSchemaUpToDate(KeycloakSession session, ClientModel client) {
AuthorizationProvider authzProvider = session.getProvider(AuthorizationProvider.class);
StoreFactory storeFactory = authzProvider.getStoreFactory();
ResourceServer resourceServer = storeFactory.getResourceServerStore().findByClient(client);
if (resourceServer == null) {
return;
}
ResourceStore resourceStore = storeFactory.getResourceStore();
ScopeStore scopeStore = storeFactory.getScopeStore();
for (Entry<String, ResourceType> entry : SCHEMA.getResourceTypes().entrySet()) {
String typeName = entry.getKey();
ResourceType type = entry.getValue();
for (String scopeName : type.getScopes()) {
if (scopeStore.findByName(resourceServer, scopeName) == null) {
scopeStore.create(resourceServer, scopeName);
}
}
if (resourceStore.findByName(resourceServer, typeName) == null) {
Resource resource = resourceStore.create(resourceServer, typeName, resourceServer.getClientId());
resource.updateScopes(type.getScopes().stream()
.map(s -> scopeStore.findByName(resourceServer, s))
.filter(Objects::nonNull)
.collect(Collectors.toSet()));
resource.setType(typeName);
}
}
}
public boolean isAdminPermissionsEnabled(RealmModel realm) {
return Profile.isFeatureEnabled(Profile.Feature.ADMIN_FINE_GRAINED_AUTHZ_V2) && realm != null && realm.isAdminPermissionsEnabled();
}
@ -408,6 +453,9 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
case USERS_RESOURCE_TYPE -> {
return resolveUser(session, resourceName).map(UserModel::getUsername).orElse(resourceType);
}
case ORGANIZATIONS_RESOURCE_TYPE -> {
return resolveOrganization(session, resourceName).map(OrganizationModel::getName).orElse(resourceType);
}
default -> throw new IllegalStateException("Resource type [" + resourceType + "] not found.");
}
}
@ -441,6 +489,8 @@ public class AdminPermissionsSchema extends AuthorizationSchema {
id = groupRemovedEvent.getGroup().getId();
} else if (event instanceof RoleRemovedEvent roleRemovedEvent) {
id = roleRemovedEvent.getRole().getId();
} else if (event instanceof OrganizationModel.OrganizationRemovedEvent orgRemovedEvent) {
id = orgRemovedEvent.getOrganization().getId();
} else {
return;
}

View file

@ -254,6 +254,8 @@ public final class PartialEvaluator {
return user.hasRole(client.getRole(AdminRoles.VIEW_USERS)) || user.hasRole(client.getRole(AdminRoles.MANAGE_USERS)) || !hasAnyQueryAdminRole(client, user);
} else if (resourceType.equals(AdminPermissionsSchema.CLIENTS)) {
return user.hasRole(client.getRole(AdminRoles.VIEW_CLIENTS)) || user.hasRole(client.getRole(AdminRoles.MANAGE_CLIENTS)) || !hasAnyQueryAdminRole(client, user);
} else if (resourceType.equals(AdminPermissionsSchema.ORGANIZATIONS)) {
return user.hasRole(client.getRole(AdminRoles.VIEW_ORGANIZATIONS)) || user.hasRole(client.getRole(AdminRoles.MANAGE_ORGANIZATIONS)) || !hasAnyQueryAdminRole(client, user);
}
return false;
@ -275,7 +277,7 @@ public final class PartialEvaluator {
private boolean hasAnyQueryAdminRole(ClientModel client, UserModel user) {
boolean result = false;
for (String adminRole : List.of(AdminRoles.QUERY_CLIENTS, AdminRoles.QUERY_GROUPS, AdminRoles.QUERY_USERS)) {
for (String adminRole : List.of(AdminRoles.QUERY_CLIENTS, AdminRoles.QUERY_GROUPS, AdminRoles.QUERY_USERS, AdminRoles.QUERY_ORGANIZATIONS)) {
RoleModel role = client.getRole(adminRole);
if (role == null) {

View file

@ -23,6 +23,7 @@ import java.util.Map;
import org.keycloak.authorization.store.syncronization.ClientApplicationSynchronizer;
import org.keycloak.authorization.store.syncronization.GroupSynchronizer;
import org.keycloak.authorization.store.syncronization.OrganizationSynchronizer;
import org.keycloak.authorization.store.syncronization.RealmSynchronizer;
import org.keycloak.authorization.store.syncronization.RoleSynchronizer;
import org.keycloak.authorization.store.syncronization.Synchronizer;
@ -30,6 +31,7 @@ import org.keycloak.authorization.store.syncronization.UserSynchronizer;
import org.keycloak.models.ClientModel.ClientRemovedEvent;
import org.keycloak.models.GroupModel.GroupRemovedEvent;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.OrganizationModel.OrganizationRemovedEvent;
import org.keycloak.models.RealmModel.RealmRemovedEvent;
import org.keycloak.models.RoleContainerModel.RoleRemovedEvent;
import org.keycloak.models.UserModel.UserRemovedEvent;
@ -54,6 +56,7 @@ public interface AuthorizationStoreFactory extends ProviderFactory<StoreFactory>
synchronizers.put(UserRemovedEvent.class, new UserSynchronizer());
synchronizers.put(GroupRemovedEvent.class, new GroupSynchronizer());
synchronizers.put(RoleRemovedEvent.class, new RoleSynchronizer());
synchronizers.put(OrganizationRemovedEvent.class, new OrganizationSynchronizer());
factory.register(event -> {
try {

View file

@ -0,0 +1,18 @@
package org.keycloak.authorization.store.syncronization;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.fgap.AdminPermissionsSchema;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.OrganizationModel.OrganizationRemovedEvent;
import org.keycloak.provider.ProviderFactory;
public class OrganizationSynchronizer implements Synchronizer<OrganizationRemovedEvent> {
@Override
public void synchronize(OrganizationRemovedEvent event, KeycloakSessionFactory factory) {
ProviderFactory<AuthorizationProvider> providerFactory = factory.getProviderFactory(AuthorizationProvider.class);
AuthorizationProvider authorizationProvider = providerFactory.create(event.getKeycloakSession());
AdminPermissionsSchema.SCHEMA.removeResourceObject(authorizationProvider, event);
}
}

View file

@ -100,6 +100,25 @@ public interface OrganizationModel {
}
}
interface OrganizationRemovedEvent extends ProviderEvent {
OrganizationModel getOrganization();
KeycloakSession getKeycloakSession();
static void fire(OrganizationModel organization, KeycloakSession session) {
session.getKeycloakSessionFactory().publish(new OrganizationRemovedEvent() {
@Override
public OrganizationModel getOrganization() {
return organization;
}
@Override
public KeycloakSession getKeycloakSession() {
return session;
}
});
}
}
String getId();
void setName(String name);

View file

@ -115,7 +115,7 @@ public class OrganizationGroupResource {
@APIResponse(responseCode = "404", description = "Not Found")
})
public void deleteGroup() {
auth.orgs().requireManage();
auth.orgs().requireManage(organization);
session.groups().removeGroup(session.getContext().getRealm(), group);
adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri()).success();
}
@ -132,7 +132,7 @@ public class OrganizationGroupResource {
@APIResponse(responseCode = "409", description = "Conflict")
})
public Response updateGroup(GroupRepresentation rep) {
auth.orgs().requireManage();
auth.orgs().requireManage(organization);
try {
String groupName = rep.getName();
@ -223,7 +223,7 @@ public class OrganizationGroupResource {
@APIResponse(responseCode = "409", description = "Conflict")
})
public Response addSubGroup(GroupRepresentation rep) {
auth.orgs().requireManage();
auth.orgs().requireManage(organization);
String groupName = rep.getName();
if (ObjectUtil.isBlank(groupName)) {
throw ErrorResponse.error("Group name is missing", Response.Status.BAD_REQUEST);
@ -328,13 +328,15 @@ public class OrganizationGroupResource {
@APIResponse(responseCode = "409", description = "Conflict - User is already a member of the group")
})
public void joinGroup(@PathParam("userId") String userId) {
auth.orgs().requireManage();
auth.orgs().requireManage(organization);
UserModel user = session.users().getUserById(session.getContext().getRealm(), userId);
if (user == null) {
throw ErrorResponse.error("User does not exist", Response.Status.NOT_FOUND);
}
auth.users().requireManageGroupMembership(user);
if (!organizationProvider.isMember(organization, user)) {
throw ErrorResponse.error("User is not member of the organization", Response.Status.BAD_REQUEST);
}
@ -370,13 +372,15 @@ public class OrganizationGroupResource {
@APIResponse(responseCode = "404", description = "Not Found - User does not exist")
})
public void leaveGroup(@PathParam("userId") String userId) {
auth.orgs().requireManage();
auth.orgs().requireManage(organization);
UserModel user = session.users().getUserById(session.getContext().getRealm(), userId);
if (user == null) {
throw ErrorResponse.error("User does not exist", Response.Status.NOT_FOUND);
}
auth.users().requireManageGroupMembership(user);
if (user.isMemberOf(group)) {
try {
user.leaveGroup(group);

View file

@ -101,7 +101,7 @@ public class OrganizationGroupsResource {
@APIResponse(responseCode = "409", description = "Conflict")
})
public Response addTopLevelGroup(GroupRepresentation rep) {
auth.orgs().requireManage();
auth.orgs().requireManage(organization);
try {
String groupName = rep.getName();
@ -195,6 +195,8 @@ public class OrganizationGroupsResource {
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation,
@QueryParam("populateHierarchy") @DefaultValue("false") boolean populateHierarchy,
@QueryParam("subGroupsCount") @DefaultValue("false") boolean subGroupsCount) {
auth.orgs().requireView(organization);
Stream<GroupModel> groups;
if (Objects.nonNull(searchQuery)) {
Map<String, String> attributes = SearchQueryUtils.getFields(searchQuery);
@ -236,6 +238,8 @@ public class OrganizationGroupsResource {
})
public GroupRepresentation getGroupByPath(@PathParam("path") String path,
@Parameter(description = "Whether to return the count of subgroups (default: false)") @QueryParam("subGroupsCount") @DefaultValue("false") boolean subGroupsCount) {
auth.orgs().requireView(organization);
GroupModel found = KeycloakModelUtils.findGroupByPath(session, realm, organization, path);
if (found == null) {
throw new NotFoundException("Group path does not exist");
@ -249,6 +253,8 @@ public class OrganizationGroupsResource {
@Path("{group-id}")
public OrganizationGroupResource getGroupById(@PathParam("group-id") String id) {
auth.orgs().requireView(organization);
GroupModel group = realm.getGroupById(id);
if (group == null) {

View file

@ -93,8 +93,9 @@ public class OrganizationIdentityProvidersResource {
@APIResponse(responseCode = "409", description = "Conflict")
})
public Response addIdentityProvider(String id) {
auth.orgs().requireManage();
auth.realm().requireManageIdentityProviders();
auth.orgs().requireManage(organization);
// todo: do we want to enforce that admins has to have manage-identity-providers admin role to be able to assign IDP to the org?
// auth.realm().requireManageIdentityProviders();
id = id.trim().replaceAll("^\"|\"$", ""); // fixes https://github.com/keycloak/keycloak/issues/34401
try {
@ -124,6 +125,9 @@ public class OrganizationIdentityProvidersResource {
@APIResponse(responseCode = "403", description = "Forbidden")
})
public Stream<IdentityProviderRepresentation> getIdentityProviders() {
auth.orgs().requireView(organization);
// todo: do we want to enforce that admins has to have view-identity-providers admin role to be able to get the IDPs linked to the org?
// auth.realm().requireViewIdentityProviders();
return organization.getIdentityProviders().map(this::toRepresentation);
}
@ -141,6 +145,9 @@ public class OrganizationIdentityProvidersResource {
@APIResponse(responseCode = "404", description = "Not Found")
})
public IdentityProviderRepresentation getIdentityProvider(@PathParam("alias") String alias) {
auth.orgs().requireView(organization);
// todo: do we want to enforce that admins has to have view-identity-providers admin role to be able to get the IDP linked to the org?
// auth.realm().requireViewIdentityProviders();
IdentityProviderModel broker = session.identityProviders().getByAlias(alias);
if (!isOrganizationBroker(broker)) {
@ -189,7 +196,7 @@ public class OrganizationIdentityProvidersResource {
@Parameter(description = "If true, return brief representation; otherwise return full representation") @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation,
@Parameter(description = "If true, include subgroups count in the response") @QueryParam("subGroupsCount") @DefaultValue("false") boolean subGroupsCount) {
// Validate that the identity provider is associated with the organization
// Validate that the identity provider is associated with the organization and the caller can view the org
getIdentityProvider(alias);
OrganizationGroupsResource groupsResource = new OrganizationGroupsResource(session, organization, adminEvent, auth);
@ -210,7 +217,9 @@ public class OrganizationIdentityProvidersResource {
@APIResponse(responseCode = "404", description = "Not Found")
})
public Response delete(@PathParam("alias") String alias) {
auth.orgs().requireManage();
auth.orgs().requireManage(organization);
// todo: do we want to enforce that admins has to have manage-identity-providers admin role to be able to unassign IDP to the org?
// auth.realm().requireManageIdentityProviders();
IdentityProviderModel broker = session.identityProviders().getByAlias(alias);
if (!isOrganizationBroker(broker)) {

View file

@ -23,6 +23,7 @@ import java.util.stream.Stream;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
@ -97,7 +98,7 @@ public class OrganizationInvitationResource {
}
public Response inviteUser(String email, String firstName, String lastName) {
auth.orgs().requireManage();
auth.orgs().requireManage(organization);
if (!organization.isEnabled()) {
throw ErrorResponse.error("Organization is disabled", Status.BAD_REQUEST);
@ -147,7 +148,7 @@ public class OrganizationInvitationResource {
}
public Response inviteExistingUser(String id) {
auth.orgs().requireManage();
auth.orgs().requireManage(organization);
if (!organization.isEnabled()) {
throw ErrorResponse.error("Organization is disabled", Status.BAD_REQUEST);
@ -160,7 +161,9 @@ public class OrganizationInvitationResource {
UserModel user = session.users().getUserById(realm, id);
if (user == null) {
throw ErrorResponse.error("User does not exist", Status.BAD_REQUEST);
throw auth.users().canQuery() ?
ErrorResponse.error("User does not exist", Status.BAD_REQUEST) :
new ForbiddenException();
}
if (StringUtil.isBlank(user.getEmail())) {

View file

@ -24,6 +24,7 @@ import java.util.stream.Stream;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
@ -102,15 +103,10 @@ public class OrganizationMemberResource {
@APIResponse(responseCode = "409", description = "Conflict")
})
public Response addMember(String id) {
auth.orgs().requireManage();
auth.orgs().requireManage(organization);
id = id.trim().replaceAll("^\"|\"$", ""); // fixes https://github.com/keycloak/keycloak/issues/34401
UserModel user = session.users().getUserById(realm, id);
if (user == null) {
throw ErrorResponse.error("User does not exist", Status.BAD_REQUEST);
}
UserModel user = getUser(id);
auth.users().requireManage(user);
try {
@ -182,6 +178,7 @@ public class OrganizationMemberResource {
) {
auth.users().requireQuery();
// if a dedicated admin can query, but cannot view (and FGAP is not enabled) - we can return empty list right away to save a roundtrip to the DB
if (!AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm) && !auth.users().canView()) {
return Stream.empty();
}
@ -234,7 +231,7 @@ public class OrganizationMemberResource {
@APIResponse(responseCode = "403", description = "Forbidden")
})
public Response delete(@PathParam("member-id") String memberId) {
auth.orgs().requireManage();
auth.orgs().requireManage(organization);
if (StringUtil.isBlank(memberId)) {
throw ErrorResponse.error("id cannot be null", Status.BAD_REQUEST);
}
@ -277,6 +274,11 @@ public class OrganizationMemberResource {
UserModel member = organization == null ? getUser(memberId) : getMember(memberId);
auth.users().requireView(member);
// if a dedicated admin can query, but cannot view (and FGAP is not enabled) - we can return empty list right away to save a roundtrip to the DB
if (!AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm) && !auth.orgs().canView()) {
return Stream.empty();
}
return provider.getByMember(member)
.map(model -> ModelToRepresentation.toRepresentation(model, briefRepresentation));
}
@ -323,6 +325,7 @@ public class OrganizationMemberResource {
public Long count() {
auth.users().requireQuery();
// if a dedicated admin can query, but cannot view (and FGAP is not enabled) - we can return 0L right away to save a roundtrip to the DB
if (!AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(realm) && !auth.users().canView()) {
return 0L;
}
@ -334,7 +337,7 @@ public class OrganizationMemberResource {
UserModel member = provider.getMemberById(organization, id);
if (member == null) {
throw new NotFoundException();
throw (auth.users().canQuery()) ? new NotFoundException() : new ForbiddenException();
}
return member;
@ -344,7 +347,7 @@ public class OrganizationMemberResource {
UserModel user = session.users().getUserById(realm, id);
if (user == null) {
throw new NotFoundException();
throw (auth.users().canQuery()) ? new NotFoundException() : new ForbiddenException();
}
return user;

View file

@ -93,7 +93,7 @@ public class OrganizationResource {
@APIResponse(responseCode = "403", description = "Forbidden")
})
public Response delete() {
auth.orgs().requireManage();
auth.orgs().requireManage(organization);
boolean removed = provider.remove(organization);
if (removed) {
adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri()).success();
@ -114,7 +114,7 @@ public class OrganizationResource {
@APIResponse(responseCode = "409", description = "Conflict")
})
public Response update(OrganizationRepresentation organizationRep) {
auth.orgs().requireManage();
auth.orgs().requireManage(organization);
// attempt to change organization name to an existing organization name
if (!Objects.equals(organization.getName(), organizationRep.getName()) &&
provider.getAllStream(organizationRep.getName(), true, -1, -1).findAny().isPresent()) {

View file

@ -23,6 +23,7 @@ import java.util.stream.Stream;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
@ -33,6 +34,7 @@ import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import org.keycloak.authorization.fgap.AdminPermissionsSchema;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.KeycloakSession;
@ -100,7 +102,7 @@ public class OrganizationsResource {
})
public Response create(OrganizationRepresentation organization) {
auth.orgs().requireManage();
Organizations.checkEnabled(provider);
Organizations.checkEnabled(provider, auth);
if (organization == null) {
throw ErrorResponse.error("Organization cannot be null.", Response.Status.BAD_REQUEST);
@ -154,9 +156,10 @@ public class OrganizationsResource {
@Parameter(description = "if false, return the full representation. Otherwise, only the basic fields are returned.") @QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation
) {
auth.orgs().requireQuery();
Organizations.checkEnabled(provider);
Organizations.checkEnabled(provider, auth);
if (!auth.orgs().canView()) {
// if a dedicated admin can query, but cannot view (and FGAP is not enabled) - we can return empty list right away to save a roundtrip to the DB
if (!AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(session.getContext().getRealm()) && !auth.orgs().canView()) {
return Stream.empty();
}
@ -174,8 +177,7 @@ public class OrganizationsResource {
*/
@Path("{org-id}")
public OrganizationResource get(@PathParam("org-id") String orgId) {
auth.orgs().requireView();
Organizations.checkEnabled(provider);
Organizations.checkEnabled(provider, auth);
if (StringUtil.isBlank(orgId)) {
throw ErrorResponse.error("Id cannot be null.", Response.Status.BAD_REQUEST);
@ -184,9 +186,12 @@ public class OrganizationsResource {
OrganizationModel organizationModel = provider.getById(orgId);
if (organizationModel == null) {
throw ErrorResponse.error("Organization not found.", Response.Status.NOT_FOUND);
throw (auth.orgs().canQuery()) ?
ErrorResponse.error("Organization not found.", Response.Status.NOT_FOUND) :
new ForbiddenException();
}
auth.orgs().requireView(organizationModel);
session.getContext().setOrganization(organizationModel);
return new OrganizationResource(session, organizationModel, adminEvent, auth);
@ -213,10 +218,11 @@ public class OrganizationsResource {
@Parameter(description = "Boolean which defines whether the param 'search' must match exactly or not") @QueryParam("exact") Boolean exact
) {
auth.orgs().requireQuery();
Organizations.checkEnabled(provider);
Organizations.checkEnabled(provider, auth);
if (!auth.orgs().canView()) {
return 0;
// if a dedicated admin can query, but cannot view (and FGAP is not enabled) - we can return 0L right away to save a roundtrip to the DB
if (!AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(session.getContext().getRealm()) && !auth.orgs().canView()) {
return 0L;
}
if (StringUtil.isNotBlank(searchQuery)) {
@ -243,11 +249,7 @@ public class OrganizationsResource {
@QueryParam("briefRepresentation") @DefaultValue("true") boolean briefRepresentation
) {
auth.orgs().requireQuery();
Organizations.checkEnabled(provider);
if (!auth.orgs().canView()) {
return Stream.empty();
}
Organizations.checkEnabled(provider, auth);
return new OrganizationMemberResource(session, null, adminEvent, auth).getOrganizations(memberId, briefRepresentation);
}

View file

@ -24,6 +24,7 @@ import java.util.Optional;
import java.util.function.Consumer;
import java.util.stream.Stream;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.ws.rs.core.Response;
@ -50,6 +51,7 @@ import org.keycloak.organization.OrganizationProvider;
import org.keycloak.organization.protocol.mappers.oidc.OrganizationScope;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.Urls;
import org.keycloak.services.resources.admin.fgap.AdminPermissionEvaluator;
import org.keycloak.sessions.AuthenticationSessionModel;
import static java.util.Optional.of;
@ -150,9 +152,11 @@ public class Organizations {
return isEnabledAndOrganizationsPresent(provider);
}
public static void checkEnabled(OrganizationProvider provider) {
public static void checkEnabled(OrganizationProvider provider, AdminPermissionEvaluator auth) {
if (provider == null || !provider.isEnabled()) {
throw ErrorResponse.error("Organizations not enabled for this realm.", Response.Status.NOT_FOUND);
throw auth.orgs().canQuery() ?
ErrorResponse.error("Organizations not enabled for this realm.", Response.Status.NOT_FOUND) :
new ForbiddenException();
}
}

View file

@ -263,7 +263,7 @@ class MgmtPermissions implements AdminPermissionEvaluator, AdminPermissionManage
@Override
public OrganizationPermissions orgs() {
if (orgPermissions != null) return orgPermissions;
orgPermissions = new OrganizationPermissions(this);
orgPermissions = new OrganizationPermissions(session, authz, this);
return orgPermissions;
}

View file

@ -19,6 +19,7 @@ package org.keycloak.services.resources.admin.fgap;
import org.keycloak.authorization.fgap.AdminPermissionsSchema;
import org.keycloak.models.ClientModel;
import org.keycloak.models.GroupModel;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
@ -72,6 +73,18 @@ sealed interface ModelRecord {
}
}
record OrganizationModelRecord(OrganizationModel organization) implements ModelRecord {
@Override
public String getResourceType() {
return AdminPermissionsSchema.ORGANIZATIONS_RESOURCE_TYPE;
}
@Override
public String getId() {
return organization == null ? null : organization.getId();
}
}
String getId();
String getResourceType();
}

View file

@ -16,16 +16,26 @@
*/
package org.keycloak.services.resources.admin.fgap;
import org.keycloak.models.OrganizationModel;
public interface OrganizationPermissionEvaluator {
boolean canManage();
boolean canManage(OrganizationModel organization);
void requireManage();
void requireManage(OrganizationModel organization);
boolean canView();
boolean canView(OrganizationModel organization);
void requireView();
void requireView(OrganizationModel organization);
boolean canQuery();
void requireQuery();

View file

@ -18,19 +18,43 @@ package org.keycloak.services.resources.admin.fgap;
import jakarta.ws.rs.ForbiddenException;
import org.keycloak.authorization.AuthorizationProvider;
import org.keycloak.authorization.fgap.AdminPermissionsSchema;
import org.keycloak.authorization.store.PolicyStore;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel;
import org.keycloak.services.resources.admin.fgap.ModelRecord.OrganizationModelRecord;
class OrganizationPermissions implements OrganizationPermissionEvaluator {
private final FineGrainedAdminPermissionEvaluator eval;
private final MgmtPermissions root;
OrganizationPermissions(MgmtPermissions root) {
OrganizationPermissions(KeycloakSession session, AuthorizationProvider authz, MgmtPermissions root) {
this.root = root;
ResourceStore resourceStore = (authz == null) ? null : authz.getStoreFactory().getResourceStore();
PolicyStore policyStore = (authz == null) ? null : authz.getStoreFactory().getPolicyStore();
this.eval = new FineGrainedAdminPermissionEvaluator(session, root, resourceStore, policyStore);
}
@Override
public boolean canManage() {
return root.hasOneAdminRole(AdminRoles.MANAGE_ORGANIZATIONS, AdminRoles.MANAGE_REALM);
if (root.hasOneAdminRole(AdminRoles.MANAGE_ORGANIZATIONS, AdminRoles.MANAGE_REALM)) {
return true;
}
return eval.hasPermission(new OrganizationModelRecord(null), null, AdminPermissionsSchema.MANAGE);
}
@Override
public boolean canManage(OrganizationModel organization) {
if (root.hasOneAdminRole(AdminRoles.MANAGE_ORGANIZATIONS, AdminRoles.MANAGE_REALM)) {
return true;
}
return eval.hasPermission(new OrganizationModelRecord(organization), null, AdminPermissionsSchema.MANAGE);
}
@Override
@ -40,10 +64,29 @@ class OrganizationPermissions implements OrganizationPermissionEvaluator {
}
}
@Override
public void requireManage(OrganizationModel organization) {
if (!canManage(organization)) {
throw new ForbiddenException();
}
}
@Override
public boolean canView() {
return root.hasOneAdminRole(AdminRoles.MANAGE_ORGANIZATIONS, AdminRoles.VIEW_ORGANIZATIONS,
AdminRoles.MANAGE_REALM);
if (root.hasOneAdminRole(AdminRoles.MANAGE_ORGANIZATIONS, AdminRoles.VIEW_ORGANIZATIONS, AdminRoles.MANAGE_REALM)) {
return true;
}
return eval.hasPermission(new OrganizationModelRecord(null), null, AdminPermissionsSchema.VIEW);
}
@Override
public boolean canView(OrganizationModel organization) {
if (root.hasOneAdminRole(AdminRoles.MANAGE_ORGANIZATIONS, AdminRoles.VIEW_ORGANIZATIONS, AdminRoles.MANAGE_REALM)) {
return true;
}
return eval.hasPermission(new OrganizationModelRecord(organization), null, AdminPermissionsSchema.VIEW);
}
@Override
@ -53,6 +96,13 @@ class OrganizationPermissions implements OrganizationPermissionEvaluator {
}
}
@Override
public void requireView(OrganizationModel organization) {
if (!canView(organization)) {
throw new ForbiddenException();
}
}
@Override
public boolean canQuery() {
return root.hasOneAdminRole(AdminRoles.QUERY_ORGANIZATIONS) || canView();

View file

@ -68,7 +68,7 @@ ManagedRealm realm;
static class MyRealmConfig implements RealmConfig {
@Override
public RealmConfigBuilder configure(RealmConfigBuilder builder) {
public RealmBuilder configure(RealmBuilder builder) {
return builder
.name("myrealm")
.groups("group-a", "group-b");

View file

@ -390,7 +390,8 @@ public class OrganizationAdminRolesPermissionsTest extends AbstractOrganizationT
}
}
@Test
// @Test
// todo do we enforce manage-identity-providers ??
public void testIdpLinkingRequiresManageIdentityProviders() {
// manage-orgs-only-admin has manage-organizations but NOT manage-identity-providers
// manage-orgs-admin has both manage-organizations AND manage-identity-providers
@ -534,8 +535,11 @@ public class OrganizationAdminRolesPermissionsTest extends AbstractOrganizationT
// count should return 0
assertThat(queryOrgsResource.organizations().count("testQueryOrg"), equalTo(0L));
// getOrganizations for a member should return empty list
assertThat(queryOrgsResource.organizations().members().getOrganizations(userId), Matchers.empty());
// getOrganizations for a member should fail - requires user view permission
try {
queryOrgsResource.organizations().members().getOrganizations(userId);
fail("Expected ForbiddenException");
} catch (ForbiddenException expected) {}
// get specific org should fail - requires view-organizations
try {

View file

@ -0,0 +1,48 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.organization.authz.fgap;
import org.keycloak.models.AdminRoles;
import org.keycloak.models.Constants;
import org.keycloak.testframework.realm.ClientBuilder;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.testframework.realm.RealmConfig;
import org.keycloak.testframework.realm.UserBuilder;
public class OrganizationFgapConfig implements RealmConfig {
@Override
public RealmBuilder configure(RealmBuilder realm) {
realm.users(UserBuilder.create()
.username("myadmin")
.name("My", "Admin")
.email("myadmin@localhost")
.emailVerified(true)
.password("password")
.clientRoles(Constants.REALM_MANAGEMENT_CLIENT_ID,
AdminRoles.QUERY_USERS,
AdminRoles.QUERY_ORGANIZATIONS).build());
realm.clients(ClientBuilder.create()
.clientId("myclient")
.secret("mysecret")
.directAccessGrantsEnabled(true).build());
return realm
.adminPermissionsEnabled(true)
.organizationsEnabled(true);
}
}

View file

@ -0,0 +1,269 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.organization.authz.fgap;
import java.util.Set;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.core.Response;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.models.Constants;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.Logic;
import org.keycloak.representations.idm.authorization.ScopePermissionRepresentation;
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
import org.keycloak.testframework.annotations.InjectAdminClient;
import org.keycloak.testframework.annotations.InjectClient;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedClient;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.util.ApiUtil;
import org.keycloak.tests.admin.authz.fgap.PermissionTestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.MANAGE;
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.ORGANIZATIONS_RESOURCE_TYPE;
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.VIEW;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.fail;
@KeycloakIntegrationTest
public class OrganizationResourceTypeEvaluationTest {
@InjectRealm(config = OrganizationFgapConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@InjectClient(attachTo = Constants.ADMIN_PERMISSIONS_CLIENT_ID)
ManagedClient client;
@InjectAdminClient(mode = InjectAdminClient.Mode.MANAGED_REALM, client = "myclient", user = "myadmin")
Keycloak realmAdminClient;
private String orgAId;
private String orgBId;
@BeforeEach
public void setup() {
orgAId = createOrg("orgA", "orga.org");
orgBId = createOrg("orgB", "orgb.org");
}
@Test
public void testCannotViewOrManageWithoutPermission() {
try {
realmAdminClient.realm(realm.getName()).organizations().get(orgAId).toRepresentation();
fail("Expected ForbiddenException");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
}
@Test
public void testViewSpecificOrganization() {
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createPermission(client, orgAId, ORGANIZATIONS_RESOURCE_TYPE, Set.of(VIEW), policy);
OrganizationRepresentation orgA = realmAdminClient.realm(realm.getName()).organizations().get(orgAId).toRepresentation();
assertNotNull(orgA);
assertEquals("orgA", orgA.getName());
try {
realmAdminClient.realm(realm.getName()).organizations().get(orgBId).toRepresentation();
fail("Expected ForbiddenException");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
}
@Test
public void testManageSpecificOrganization() {
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createPermission(client, orgAId, ORGANIZATIONS_RESOURCE_TYPE, Set.of(VIEW, MANAGE), policy);
OrganizationResource orgAResource = realmAdminClient.realm(realm.getName()).organizations().get(orgAId);
OrganizationRepresentation orgARep = orgAResource.toRepresentation();
orgARep.setName("orgA-updated");
try (Response response = orgAResource.update(orgARep)) {
assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
OrganizationRepresentation orgBRep = new OrganizationRepresentation();
orgBRep.setName("orgB-updated");
try (Response response = realmAdminClient.realm(realm.getName()).organizations().get(orgBId).update(orgBRep)) {
assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
}
}
@Test
public void testDeleteSpecificOrganization() {
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createPermission(client, orgAId, ORGANIZATIONS_RESOURCE_TYPE, Set.of(VIEW, MANAGE), policy);
try (Response response = realmAdminClient.realm(realm.getName()).organizations().get(orgAId).delete()) {
assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
try (Response response = realmAdminClient.realm(realm.getName()).organizations().get(orgBId).delete()) {
assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
}
}
@Test
public void testViewAllOrganizations() {
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createAllPermission(client, ORGANIZATIONS_RESOURCE_TYPE, policy, Set.of(VIEW));
OrganizationRepresentation orgA = realmAdminClient.realm(realm.getName()).organizations().get(orgAId).toRepresentation();
assertNotNull(orgA);
OrganizationRepresentation orgB = realmAdminClient.realm(realm.getName()).organizations().get(orgBId).toRepresentation();
assertNotNull(orgB);
}
@Test
public void testManageAllOrganizations() {
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createAllPermission(client, ORGANIZATIONS_RESOURCE_TYPE, policy, Set.of(VIEW, MANAGE));
OrganizationResource orgAResource = realmAdminClient.realm(realm.getName()).organizations().get(orgAId);
OrganizationRepresentation orgARep = orgAResource.toRepresentation();
orgARep.setName("orgA-updated");
try (Response response = orgAResource.update(orgARep)) {
assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
OrganizationResource orgBResource = realmAdminClient.realm(realm.getName()).organizations().get(orgBId);
OrganizationRepresentation orgBRep = orgBResource.toRepresentation();
orgBRep.setName("orgB-updated");
try (Response response = orgBResource.update(orgBRep)) {
assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
}
@Test
public void testDenyOverridesAllowAll() {
UserPolicyRepresentation allowPolicy = createAdminPolicy();
PermissionTestUtils.createAllPermission(client, ORGANIZATIONS_RESOURCE_TYPE, allowPolicy, Set.of(VIEW, MANAGE));
OrganizationRepresentation orgARep = realmAdminClient.realm(realm.getName()).organizations().get(orgAId).toRepresentation();
assertNotNull(orgARep);
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
UserPolicyRepresentation denyPolicy = PermissionTestUtils.createUserPolicy(Logic.NEGATIVE, realm, client, "Deny Admin " + KeycloakModelUtils.generateId(), myadmin.getId());
PermissionTestUtils.createPermission(client, orgAId, ORGANIZATIONS_RESOURCE_TYPE, Set.of(VIEW), denyPolicy);
try {
realmAdminClient.realm(realm.getName()).organizations().get(orgAId).toRepresentation();
fail("Expected ForbiddenException");
} catch (Exception ex) {
assertThat(ex, instanceOf(ForbiddenException.class));
}
OrganizationRepresentation orgBRep = realmAdminClient.realm(realm.getName()).organizations().get(orgBId).toRepresentation();
assertNotNull(orgBRep);
}
@Test
public void testViewDoesNotGrantManage() {
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createPermission(client, orgAId, ORGANIZATIONS_RESOURCE_TYPE, Set.of(VIEW), policy);
OrganizationRepresentation orgARep = realmAdminClient.realm(realm.getName()).organizations().get(orgAId).toRepresentation();
assertNotNull(orgARep);
orgARep.setName("orgA-updated");
try (Response response = realmAdminClient.realm(realm.getName()).organizations().get(orgAId).update(orgARep)) {
assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
}
}
@Test
public void testCreateRequiresGlobalManage() {
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createAllPermission(client, ORGANIZATIONS_RESOURCE_TYPE, policy, Set.of(VIEW, MANAGE));
OrganizationRepresentation newOrg = new OrganizationRepresentation();
newOrg.setName("orgC");
newOrg.setAlias("orgC");
OrganizationDomainRepresentation domain = new OrganizationDomainRepresentation();
domain.setName("orgc.org");
newOrg.addDomain(domain);
try (Response response = realmAdminClient.realm(realm.getName()).organizations().create(newOrg)) {
assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
}
}
@Test
public void testAllResourcePermissionScopeChange() {
UserPolicyRepresentation policy = createAdminPolicy();
ScopePermissionRepresentation allPerm = PermissionTestUtils.createAllPermission(client, ORGANIZATIONS_RESOURCE_TYPE, policy, Set.of(VIEW, MANAGE));
realmAdminClient.realm(realm.getName()).organizations().get(orgAId).toRepresentation();
OrganizationRepresentation orgARep = realmAdminClient.realm(realm.getName()).organizations().get(orgAId).toRepresentation();
orgARep.setName("orgA-updated");
try (Response response = realmAdminClient.realm(realm.getName()).organizations().get(orgAId).update(orgARep)) {
assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
allPerm = client.admin().authorization().permissions().scope().findByName(allPerm.getName());
allPerm.setScopes(Set.of(VIEW));
client.admin().authorization().permissions().scope().findById(allPerm.getId()).update(allPerm);
realmAdminClient.realm(realm.getName()).organizations().get(orgAId).toRepresentation();
orgARep = realmAdminClient.realm(realm.getName()).organizations().get(orgAId).toRepresentation();
orgARep.setName("orgA-updated2");
try (Response response = realmAdminClient.realm(realm.getName()).organizations().get(orgAId).update(orgARep)) {
assertEquals(Response.Status.FORBIDDEN.getStatusCode(), response.getStatus());
}
}
// -- helpers --
private String createOrg(String name, String domainName) {
OrganizationRepresentation orgRep = new OrganizationRepresentation();
orgRep.setName(name);
orgRep.setAlias(name);
OrganizationDomainRepresentation domain = new OrganizationDomainRepresentation();
domain.setName(domainName);
orgRep.addDomain(domain);
try (Response response = realm.admin().organizations().create(orgRep)) {
assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
return ApiUtil.getCreatedId(response);
}
}
private UserPolicyRepresentation createAdminPolicy() {
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
return PermissionTestUtils.createUserPolicy(realm, client, "Allow My Admin " + KeycloakModelUtils.generateId(), myadmin.getId());
}
}

View file

@ -0,0 +1,248 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.organization.authz.fgap;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import jakarta.ws.rs.core.Response;
import org.keycloak.admin.client.Keycloak;
import org.keycloak.authorization.fgap.AdminPermissionsSchema;
import org.keycloak.models.Constants;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.OrganizationDomainRepresentation;
import org.keycloak.representations.idm.OrganizationRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.authorization.Logic;
import org.keycloak.representations.idm.authorization.UserPolicyRepresentation;
import org.keycloak.testframework.annotations.InjectAdminClient;
import org.keycloak.testframework.annotations.InjectClient;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.InjectUser;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedClient;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.ManagedUser;
import org.keycloak.testframework.util.ApiUtil;
import org.keycloak.tests.admin.authz.fgap.PermissionTestUtils;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.MANAGE;
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.ORGANIZATIONS_RESOURCE_TYPE;
import static org.keycloak.authorization.fgap.AdminPermissionsSchema.VIEW;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasSize;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@KeycloakIntegrationTest
public class OrganizationResourceTypeFilteringTest {
@InjectRealm(config = OrganizationFgapConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@InjectClient(attachTo = Constants.ADMIN_PERMISSIONS_CLIENT_ID)
ManagedClient client;
@InjectAdminClient(mode = InjectAdminClient.Mode.MANAGED_REALM, client = "myclient", user = "myadmin")
Keycloak realmAdminClient;
@InjectUser(ref = "alice")
ManagedUser userAlice;
private String orgAId;
private String orgBId;
private String orgCId;
@BeforeEach
public void setup() {
orgAId = createOrg("orgA", "orga.org");
orgBId = createOrg("orgB", "orgb.org");
orgCId = createOrg("orgC", "orgc.org");
}
@Test
public void testSearchReturnsEmptyWithNoPermissions() {
List<OrganizationRepresentation> result = realmAdminClient.realm(realm.getName()).organizations().search(null, null, -1, -1);
assertTrue(result.isEmpty());
}
@Test
public void testCountReturnsZeroWithNoPermissions() {
long count = realmAdminClient.realm(realm.getName()).organizations().count(null);
assertThat(count, equalTo(0L));
}
@Test
public void testSearchReturnsOnlyPermittedOrganizations() {
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createPermission(client, Set.of(orgAId, orgBId), ORGANIZATIONS_RESOURCE_TYPE, Set.of(VIEW), policy);
List<OrganizationRepresentation> result = realmAdminClient.realm(realm.getName()).organizations().search(null, null, -1, -1);
assertThat(result, hasSize(2));
Set<String> names = result.stream().map(OrganizationRepresentation::getName).collect(Collectors.toSet());
assertTrue(names.contains("orgA"));
assertTrue(names.contains("orgB"));
}
@Test
public void testCountReturnsOnlyPermittedOrganizations() {
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createPermission(client, orgAId, ORGANIZATIONS_RESOURCE_TYPE, Set.of(VIEW), policy);
long count = realmAdminClient.realm(realm.getName()).organizations().count(null);
assertThat(count, equalTo(1L));
}
@Test
public void testSearchWithAllOrganizationsPermission() {
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createAllPermission(client, ORGANIZATIONS_RESOURCE_TYPE, policy, Set.of(VIEW));
List<OrganizationRepresentation> result = realmAdminClient.realm(realm.getName()).organizations().search(null, null, -1, -1);
assertThat(result, hasSize(3));
}
@Test
public void testCountWithAllOrganizationsPermission() {
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createAllPermission(client, ORGANIZATIONS_RESOURCE_TYPE, policy, Set.of(VIEW));
long count = realmAdminClient.realm(realm.getName()).organizations().count(null);
assertThat(count, equalTo(3L));
}
@Test
public void testDenyRemovesFromSearchResults() {
UserPolicyRepresentation allowPolicy = createAdminPolicy();
PermissionTestUtils.createAllPermission(client, ORGANIZATIONS_RESOURCE_TYPE, allowPolicy, Set.of(VIEW));
List<OrganizationRepresentation> result = realmAdminClient.realm(realm.getName()).organizations().search(null, null, -1, -1);
assertThat(result, hasSize(3));
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
UserPolicyRepresentation denyPolicy = PermissionTestUtils.createUserPolicy(Logic.NEGATIVE, realm, client, "Deny Admin " + KeycloakModelUtils.generateId(), myadmin.getId());
PermissionTestUtils.createPermission(client, orgBId, ORGANIZATIONS_RESOURCE_TYPE, Set.of(VIEW), denyPolicy);
result = realmAdminClient.realm(realm.getName()).organizations().search(null, null, -1, -1);
assertThat(result, hasSize(2));
assertTrue(result.stream().map(OrganizationRepresentation::getId).noneMatch(orgBId::equals));
}
@Test
public void testDenyReducesCount() {
UserPolicyRepresentation allowPolicy = createAdminPolicy();
PermissionTestUtils.createAllPermission(client, ORGANIZATIONS_RESOURCE_TYPE, allowPolicy, Set.of(VIEW));
assertThat(realmAdminClient.realm(realm.getName()).organizations().count(null), equalTo(3L));
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
UserPolicyRepresentation denyPolicy = PermissionTestUtils.createUserPolicy(Logic.NEGATIVE, realm, client, "Deny Admin " + KeycloakModelUtils.generateId(), myadmin.getId());
PermissionTestUtils.createPermission(client, orgBId, ORGANIZATIONS_RESOURCE_TYPE, Set.of(VIEW), denyPolicy);
assertThat(realmAdminClient.realm(realm.getName()).organizations().count(null), equalTo(2L));
}
@Test
public void testSearchByName() {
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createPermission(client, Set.of(orgAId, orgCId), ORGANIZATIONS_RESOURCE_TYPE, Set.of(VIEW), policy);
List<OrganizationRepresentation> result = realmAdminClient.realm(realm.getName()).organizations().search("orgA", true, -1, -1);
assertThat(result, hasSize(1));
assertEquals("orgA", result.get(0).getName());
result = realmAdminClient.realm(realm.getName()).organizations().search("orgB", true, -1, -1);
assertTrue(result.isEmpty());
}
@Test
public void testGetByMemberRespectsPermissions() {
realm.admin().organizations().get(orgAId).members().addMember(userAlice.getId()).close();
realm.admin().organizations().get(orgBId).members().addMember(userAlice.getId()).close();
realm.admin().organizations().get(orgCId).members().addMember(userAlice.getId()).close();
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createPermission(client, userAlice.getId(), AdminPermissionsSchema.USERS_RESOURCE_TYPE, Set.of(VIEW), policy);
PermissionTestUtils.createPermission(client, Set.of(orgAId, orgCId), ORGANIZATIONS_RESOURCE_TYPE, Set.of(VIEW), policy);
List<OrganizationRepresentation> orgs = realmAdminClient.realm(realm.getName()).organizations().members().getOrganizations(userAlice.getId());
assertThat(orgs, hasSize(2));
Set<String> names = orgs.stream().map(OrganizationRepresentation::getName).collect(Collectors.toSet());
assertTrue(names.contains("orgA"));
assertTrue(names.contains("orgC"));
}
@Test
public void testGetByMemberWithAllOrganizationsPermission() {
realm.admin().organizations().get(orgAId).members().addMember(userAlice.getId()).close();
realm.admin().organizations().get(orgBId).members().addMember(userAlice.getId()).close();
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createPermission(client, userAlice.getId(), AdminPermissionsSchema.USERS_RESOURCE_TYPE, Set.of(VIEW), policy);
PermissionTestUtils.createAllPermission(client, ORGANIZATIONS_RESOURCE_TYPE, policy, Set.of(VIEW));
List<OrganizationRepresentation> orgs = realmAdminClient.realm(realm.getName()).organizations().members().getOrganizations(userAlice.getId());
assertThat(orgs, hasSize(2));
}
@Test
public void testDeleteCleansUpFgapResources() {
UserPolicyRepresentation policy = createAdminPolicy();
PermissionTestUtils.createPermission(client, Set.of(orgAId, orgBId, orgCId), ORGANIZATIONS_RESOURCE_TYPE, Set.of(VIEW, MANAGE), policy);
List<OrganizationRepresentation> result = realmAdminClient.realm(realm.getName()).organizations().search(null, null, -1, -1);
assertThat(result, hasSize(3));
try (Response response = realmAdminClient.realm(realm.getName()).organizations().get(orgAId).delete()) {
assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
}
result = realmAdminClient.realm(realm.getName()).organizations().search(null, null, -1, -1);
assertThat(result, hasSize(2));
assertTrue(result.stream().map(OrganizationRepresentation::getId).noneMatch(orgAId::equals));
}
// -- helpers --
private String createOrg(String name, String domainName) {
OrganizationRepresentation orgRep = new OrganizationRepresentation();
orgRep.setName(name);
orgRep.setAlias(name);
OrganizationDomainRepresentation domain = new OrganizationDomainRepresentation();
domain.setName(domainName);
orgRep.addDomain(domain);
try (Response response = realm.admin().organizations().create(orgRep)) {
assertThat(response.getStatus(), equalTo(Response.Status.CREATED.getStatusCode()));
return ApiUtil.getCreatedId(response);
}
}
private UserPolicyRepresentation createAdminPolicy() {
UserRepresentation myadmin = realm.admin().users().search("myadmin").get(0);
return PermissionTestUtils.createUserPolicy(realm, client, "Allow My Admin " + KeycloakModelUtils.generateId(), myadmin.getId());
}
}