8000 Introduce ORGANIZATIONS resource type in Fine-Grained Admin Permissions by vramik · Pull Request #48320 · keycloak/keycloak · GitHub
[go: up one dir, main page]

Skip to content
Merged
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
8 changes: 4 additions & 4 deletions docs/documentation/release_notes/topics/26_7_0.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ 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).
* `query-organizations` — grants the ability to search and list organizations without full view access, consistent with the `query-users` / `query-clients` / `query-groups` pattern.

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
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;
Expand Down Expand Up @@ -260,6 +261,10 @@ public UserModel getMemberById(OrganizationModel organization, String id) {

@Override
public Stream<OrganizationModel> getByMember(UserModel member) {
if (AdminPermissionsSchema.SCHEMA.isAdminPermissionsEnabled(getRealm())) {
return getCacheDelegates(getDelegate().getByMember(member));
}

if (userCache == null) {
return getDelegate().getByMember(member);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,7 @@
@NamedQuery(name="getByDomainName", query="select distinct o from OrganizationEntity o inner join OrganizationDo 10000 mainEntity 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 {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ public boolean remove(OrganizationModel organization) {
organization.getIdentityProviders().forEach((model) -> removeIdentityProvider(organization, model));
}

OrganizationModel.OrganizationRemovedEvent.fire(organization, session);

em.remove(entity);
} finally {
session.getContext().setOrganization(null);
Expand Down Expand Up @@ -251,9 +253,12 @@ public Stream<OrganizationModel> getAllStream(String search, Boolean exact, Inte
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)));
Expand All @@ -265,10 +270,12 @@ public Stream<OrganizationModel> getAllStream(Map<String, String> attributes, In
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));
}
Expand All @@ -279,9 +286,12 @@ public long count(String search, Boolean exact) {
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();
}
Expand All @@ -292,25 +302,27 @@ public long count(Map<String, String> attributes) {
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();
}

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,
Expand Down Expand Up @@ -488,22 +500,29 @@ public UserModel getMemberById(OrganizationModel organization, String id) {
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
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;
Expand Down Expand Up @@ -46,6 +47,7 @@ public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepres
@Override
public void migrateRealm(KeycloakSession session, RealmModel realm) {
updatePasswordAfterEmailVerificationDuringRegistrationOfUsers(realm);
updateAdminPermissionsSchema(session, realm);
}

private void updatePasswordAfterEmailVerificationDuringRegistrationOfUsers(RealmModel realm) {
Expand Down Expand Up @@ -116,4 +118,10 @@ private void addQueryCompositeRoles(ClientModel client) {
viewOrganizations.addCompositeRole(queryOrganizations);
}
}

private void updateAdminPermissionsSchema(KeycloakSession session, RealmModel realm) {
if (realm.getAdminPermissionsClient() != null) {
AdminPermissionsSchema.SCHEMA.init(session, realm);
}
}
}
Loading
Loading
0