created endpoint for effective client roles (#3165)

This commit is contained in:
Erik Jan de Wit 2022-08-24 10:46:22 +02:00 committed by GitHub
parent 711a780be6
commit b5ea27c194
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 306 additions and 83 deletions

View file

@ -9,7 +9,7 @@ import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider
import org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator
class AdminUIRestEndpointProvider : AdminRealmResourceProviderFactory, AdminRealmResourceProvider {
class AvailableRoleMappingProvider : AdminRealmResourceProviderFactory, AdminRealmResourceProvider {
override fun create(session: KeycloakSession): AdminRealmResourceProvider {
return this
}
@ -18,7 +18,7 @@ class AdminUIRestEndpointProvider : AdminRealmResourceProviderFactory, AdminReal
override fun postInit(factory: KeycloakSessionFactory) {}
override fun close() {}
override fun getId(): String {
return "admin-ui"
return "admin-ui-available-roles"
}
override fun getResource(
@ -27,6 +27,6 @@ class AdminUIRestEndpointProvider : AdminRealmResourceProviderFactory, AdminReal
auth: AdminPermissionEvaluator,
adminEvent: AdminEventBuilder
): Any {
return AdminUIExtendedResource(realm, auth)
return AvailableRoleMappingResource(realm, auth)
}
}

View file

@ -20,10 +20,10 @@ import javax.ws.rs.core.Context
import javax.ws.rs.core.MediaType
@Path("/")
open class AdminUIExtendedResource(
open class AvailableRoleMappingResource(
private var realm: RealmModel,
private var auth: AdminPermissionEvaluator,
) {
) : RoleMappingResource(realm, auth) {
@Context
var session: KeycloakSession? = null
@ -54,7 +54,7 @@ open class AdminUIExtendedResource(
val scopeContainer = realm.getClientScopeById(id) ?: throw NotFoundException("Could not find client scope")
auth.clients().requireView(scopeContainer)
return availableMapping(Predicate<RoleModel?> { r -> scopeContainer.hasDirectScope(r) }.negate(), first, max, search)
return mapping(Predicate<RoleModel?> { r -> scopeContainer.hasDirectScope(r) }.negate(), first, max, search)
}
@GET
@ -84,7 +84,7 @@ open class AdminUIExtendedResource(
val scopeContainer = realm.getClientById(id) ?: throw NotFoundException("Could not find client")
auth.clients().requireView(scopeContainer)
return availableMapping(Predicate<RoleModel?> { r -> scopeContainer.hasDirectScope(r) }.negate(), first, max, search)
return mapping(Predicate<RoleModel?> { r -> scopeContainer.hasDirectScope(r) }.negate(), first, max, search)
}
@GET
@ -114,7 +114,7 @@ open class AdminUIExtendedResource(
val scopeContainer = realm.getGroupById(id) ?: throw NotFoundException("Could not find group")
auth.groups().requireView(scopeContainer)
return availableMapping(Predicate<RoleModel?> { r -> scopeContainer.hasDirectRole(r) }.negate(), first, max, search)
return mapping(Predicate<RoleModel?> { r -> scopeContainer.hasDirectRole(r) }.negate(), first, max, search)
}
@ -146,7 +146,7 @@ open class AdminUIExtendedResource(
?: if (auth.users().canQuery()) throw NotFoundException("User not found") else throw ForbiddenException()
auth.users().requireView(user)
return availableMapping(Predicate<RoleModel?> { r -> user.hasDirectRole(r) }.negate(), first, max, search)
return mapping(Predicate<RoleModel?> { r -> user.hasDirectRole(r) }.negate(), first, max, search)
}
@ -173,33 +173,6 @@ open class AdminUIExtendedResource(
@QueryParam("max") @DefaultValue("10") max: Long,
@QueryParam("search") @DefaultValue("") search: String
): List<ClientRole> {
val clients = realm.clientsStream
val mapper = Mappers.getMapper(RoleMapper::class.java)
return clients
.flatMap { c -> c.rolesStream }
.map { r -> mapper.convertToRepresentation(r, realm.clientsStream) }
.skip(first)
.limit(max)
.collect(Collectors.toList()) ?: Collections.emptyList()
}
private fun availableMapping(
predicate: Predicate<RoleModel?>,
first: Long,
max: Long,
search: String
): List<ClientRole> {
val clients = realm.clientsStream
val mapper = Mappers.getMapper(RoleMapper::class.java)
return clients
.flatMap { c -> c.rolesStream }
.filter(predicate)
.filter(auth.roles()::canMapClientScope)
.map { r -> mapper.convertToRepresentation(r, realm.clientsStream) }
.filter { r -> r.client?.indexOf(search) != -1 || r.role.indexOf(search) != -1 }
.skip(first)
.limit(max)
.collect(Collectors.toList()) ?: Collections.emptyList()
return mapping({ true }, first, max, search)
}
}

View file

@ -0,0 +1,32 @@
package org.keycloak.admin.ui.rest
import org.keycloak.Config
import org.keycloak.models.KeycloakSession
import org.keycloak.models.KeycloakSessionFactory
import org.keycloak.models.RealmModel
import org.keycloak.services.resources.admin.AdminEventBuilder
import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider
import org.keycloak.services.resources.admin.ext.AdminRealmResourceProviderFactory
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator
class EffectiveRoleMappingProvider : AdminRealmResourceProviderFactory, AdminRealmResourceProvider {
override fun create(session: KeycloakSession): AdminRealmResourceProvider {
return this
}
override fun init(config: Config.Scope) {}
override fun postInit(factory: KeycloakSessionFactory) {}
override fun close() {}
override fun getId(): String {
return "admin-ui-effective-roles"
}
override fun getResource(
session: KeycloakSession,
realm: RealmModel,
auth: AdminPermissionEvaluator,
adminEvent: AdminEventBuilder
): Any {
return EffectiveRoleMappingResource(realm, auth)
}
}

View file

@ -0,0 +1,156 @@
package org.keycloak.admin.ui.rest
import org.eclipse.microprofile.openapi.annotations.Operation
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType
import org.eclipse.microprofile.openapi.annotations.media.Content
import org.eclipse.microprofile.openapi.annotations.media.Schema
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse
import org.keycloak.admin.ui.rest.model.ClientRole
import org.keycloak.models.KeycloakSession
import org.keycloak.models.RealmModel
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator
import java.util.stream.Collectors
import javax.ws.rs.*
import javax.ws.rs.core.Context
import javax.ws.rs.core.MediaType
@Path("/")
open class EffectiveRoleMappingResource(
private var realm: RealmModel,
private var auth: AdminPermissionEvaluator,
) : RoleMappingResource(realm, auth) {
@Context
var session: KeycloakSession? = null
@GET
@Path("/clientScopes/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "List all effective roles for this client scope",
description = "This endpoint returns all the client role mapping for a specific client scope"
)
@APIResponse(
responseCode = "200",
description = "",
content = [Content(
schema = Schema(
type = SchemaType.ARRAY,
implementation = ClientRole::class
)
)]
)
fun listCompositeClientScopeRoleMappings(
@PathParam("id") id: String,
): List<ClientRole> {
val scopeContainer = realm.getClientScopeById(id) ?: throw NotFoundException("Could not find client scope")
auth.clients().requireView(scopeContainer)
return mapping(scopeContainer::hasScope).collect(Collectors.toList())
}
@GET
@Path("/clients/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "List all effective roles for this client",
description = "This endpoint returns all the client role mapping for a specific client"
)
@APIResponse(
responseCode = "200",
description = "",
content = [Content(
schema = Schema(
type = SchemaType.ARRAY,
implementation = ClientRole::class
)
)]
)
fun listCompositeClientsRoleMappings(
@PathParam("id") id: String,
): List<ClientRole> {
val scopeContainer = realm.getClientById(id) ?: throw NotFoundException("Could not find client")
auth.clients().requireView(scopeContainer)
return mapping(scopeContainer::hasScope).collect(Collectors.toList())
}
@GET
@Path("/groups/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "List all effective roles for this group",
description = "This endpoint returns all the client role mapping for a specific group"
)
@APIResponse(
responseCode = "200",
description = "",
content = [Content(
schema = Schema(
type = SchemaType.ARRAY,
implementation = ClientRole::class
)
)]
)
fun listCompositeGroupsRoleMappings(
@PathParam("id") id: String,
): List<ClientRole> {
val scopeContainer = realm.getGroupById(id) ?: throw NotFoundException("Could not find group")
return mapping(scopeContainer::hasDirectRole).collect(Collectors.toList())
}
@GET
@Path("/users/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "List all effective roles for this users",
description = "This endpoint returns all the client role mapping for a specific users"
)
@APIResponse(
responseCode = "200",
description = "",
content = [Content(
schema = Schema(
type = SchemaType.ARRAY,
implementation = ClientRole::class
)
)]
)
fun listCompositeUsersRoleMappings(
@PathParam("id") id: String,
): List<ClientRole> {
val user = session?.users()?.getUserById(realm, id)
?: if (auth.users().canQuery()) throw NotFoundException("User not found") else throw ForbiddenException()
auth.users().requireView(user)
return mapping(user::hasDirectRole).collect(Collectors.toList())
}
@GET
@Path("/roles/{id}")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "List all effective roles for this realm role",
description = "This endpoint returns all the client role mapping for a specific realm role"
)
@APIResponse(
responseCode = "200",
description = "",
content = [Content(
schema = Schema(
type = SchemaType.ARRAY,
implementation = ClientRole::class
)
)]
)
fun listCompositeRealmRoleMappings(
): List<ClientRole> {
return mapping { true }.collect(Collectors.toList())
}
}

View file

@ -0,0 +1,45 @@
package org.keycloak.admin.ui.rest
import org.keycloak.admin.ui.rest.model.ClientRole
import org.keycloak.admin.ui.rest.model.RoleMapper
import org.keycloak.models.RealmModel
import org.keycloak.models.RoleModel
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator
import org.mapstruct.factory.Mappers
import java.util.*
import java.util.function.Predicate
import java.util.stream.Collectors
import java.util.stream.Stream
abstract class RoleMappingResource(
private var realm: RealmModel,
private var auth: AdminPermissionEvaluator,
) {
fun mapping(
predicate: Predicate<RoleModel?>,
): Stream<ClientRole> {
val mapper = Mappers.getMapper(RoleMapper::class.java)
return realm.clientsStream
.flatMap { c -> c.rolesStream }
.filter(predicate)
.filter(auth.roles()::canMapClientScope)
.map { r -> mapper.convertToRepresentation(r, realm.clientsStream) }
}
fun mapping(
predicate: Predicate<RoleModel?>,
first: Long,
max: Long,
search: String
): List<ClientRole> {
return mapping(predicate)
.filter { r -> r.client!!.contains(search, true) || r.role.contains(search, true) }
.skip(if (search.isBlank()) first else 0)
.limit(max)
.collect(Collectors.toList()) ?: Collections.emptyList()
}
}

View file

@ -6,6 +6,7 @@ data class ClientRole(
@field:Schema(required = true) var id: String,
@field:Schema(required = true) var role: String,
@field:Schema(required = true) var client: String?,
@field:Schema(required = true) var clientId: String?,
var description: String?
) {
}

View file

@ -14,6 +14,8 @@ abstract class RoleMapper {
@AfterMapping
fun convert(role: RoleModel, @MappingTarget clientRole: ClientRole, @Context list: Stream<ClientModel>) {
clientRole.client = list.filter { c -> role.containerId == c.id }.findFirst().get().clientId
val clientModel = list.filter { c -> role.containerId == c.id }.findFirst().get()
clientRole.clientId = clientModel.id
clientRole.client = clientModel.clientId
}
}

View file

@ -15,4 +15,5 @@
# limitations under the License.
#
org.keycloak.admin.ui.rest.AdminUIRestEndpointProvider
org.keycloak.admin.ui.rest.AvailableRoleMappingProvider
org.keycloak.admin.ui.rest.EffectiveRoleMappingProvider

View file

@ -89,7 +89,7 @@ export const AddRoleMappingModal = ({
return localeSort(
roles.map((e) => ({
client: { clientId: e.client },
client: { clientId: e.client, id: e.clientId },
role: { id: e.id, name: e.role, description: e.description },
})),
compareRow

View file

@ -20,12 +20,9 @@ import { useAlerts } from "../alert/Alerts";
import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog";
import { useAdminClient } from "../../context/auth/AdminClient";
import { ListEmptyState } from "../list-empty-state/ListEmptyState";
import {
deleteMapping,
getEffectiveClientRoles,
getEffectiveRoles,
getMapping,
} from "./queries";
import { deleteMapping, getEffectiveRoles, getMapping } from "./queries";
import { getEffectiveClientRoles } from "./resource";
import { useRealm } from "../../context/realm-context/RealmContext";
import "./role-mapping.css";
@ -92,6 +89,7 @@ export const RoleMapping = ({
}: RoleMappingProps) => {
const { t } = useTranslation(type);
const { adminClient } = useAdminClient();
const { realm } = useRealm();
const { addAlert, addError } = useAlerts();
const [key, setKey] = useState(0);
@ -107,23 +105,28 @@ export const RoleMapping = ({
};
const loader = async () => {
const effectiveRoles = await getEffectiveRoles(adminClient, type, id);
let effectiveRoles: Row[] = [];
let effectiveClientRoles: Row[] = [];
if (!hide) {
const clients = await adminClient.clients.find();
effectiveRoles = await getEffectiveRoles(adminClient, type, id);
effectiveClientRoles = (
await Promise.all(
clients.map(async (client) =>
getEffectiveClientRoles(adminClient, type, id, client)
)
)
).flat();
await getEffectiveClientRoles({
adminClient,
realm,
type,
id,
})
).map((e) => ({
client: { clientId: e.client, id: e.clientId },
role: { id: e.id, name: e.role, description: e.description },
}));
}
const roles = await getMapping(adminClient, type, id);
const realmRoles = roles.realmMappings?.map((role) => ({ role }));
const client = Object.values(roles.clientMappings || {})
const realmRolesMapping =
roles.realmMappings?.map((role) => ({ role })) || [];
const clientMapping = Object.values(roles.clientMappings || {})
.map((client) =>
client.mappings.map((role: RoleRepresentation) => ({
client: { clientId: client.client, ...client },
@ -133,8 +136,11 @@ export const RoleMapping = ({
.flat();
return [
...mapRoles(realmRoles || [], effectiveRoles, hide),
...[...client, ...effectiveClientRoles],
...mapRoles(
[...realmRolesMapping, ...clientMapping],
[...effectiveClientRoles, ...effectiveRoles],
hide
),
];
};

View file

@ -1,6 +1,5 @@
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import type MappingsRepresentation from "@keycloak/keycloak-admin-client/lib/defs/mappingsRepresentation";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type { ClientScopes } from "@keycloak/keycloak-admin-client/lib/resources/clientScopes";
import type { Groups } from "@keycloak/keycloak-admin-client/lib/resources/groups";
import type { Roles } from "@keycloak/keycloak-admin-client/lib/resources/roles";
@ -155,22 +154,6 @@ export const getEffectiveRoles = async (
}));
};
export const getEffectiveClientRoles = async (
adminClient: KeycloakAdminClient,
type: ResourcesKey,
id: string,
client: ClientRepresentation
): Promise<Row[]> => {
const query = mapping[type]!.listEffective[2];
return (
await applyQuery(adminClient, type, query, {
id,
client: client.id,
clientUniqueId: client.id,
})
).map((role) => ({ role, client: { clientId: client.id, ...client } }));
};
export const getAvailableRoles = async (
adminClient: KeycloakAdminClient,
type: ResourcesKey,

View file

@ -2,24 +2,35 @@ import KeycloakAdminClient from "@keycloak/keycloak-admin-client";
import { addTrailingSlash } from "../../util";
import { getAuthorizationHeaders } from "../../utils/getAuthorizationHeaders";
type AvailableClientRolesQuery = {
type BaseClientRolesQuery = {
adminClient: KeycloakAdminClient;
id: string;
realm: string;
type: string;
};
type AvailableClientRolesQuery = BaseClientRolesQuery & {
first: number;
max: number;
search?: string;
};
type EffectiveClientRolesQuery = BaseClientRolesQuery;
type Query = Partial<Omit<AvailableClientRolesQuery, "adminClient">> & {
adminClient: KeycloakAdminClient;
endpoint: string;
};
type ClientRole = {
id: string;
role: string;
description?: string;
client?: string;
client: string;
clientId: string;
};
export const getAvailableClientRoles = async ({
const fetchRoles = async ({
adminClient,
id,
realm,
@ -27,16 +38,17 @@ export const getAvailableClientRoles = async ({
first,
max,
search,
}: AvailableClientRolesQuery): Promise<ClientRole[]> => {
endpoint,
}: Query): Promise<ClientRole[]> => {
const accessToken = await adminClient.getAccessToken();
const baseUrl = adminClient.baseUrl;
const response = await fetch(
`${addTrailingSlash(
baseUrl
)}admin/realms/${realm}/admin-ui/${type}/${id}?first=${first}&max=${max}${
search ? "&search=" + search : ""
}`,
)}admin/realms/${realm}/admin-ui-${endpoint}/${type}/${id}?first=${
first || 0
}&max=${max || 10}${search ? "&search=" + search : ""}`,
{
method: "GET",
headers: getAuthorizationHeaders(accessToken),
@ -45,3 +57,15 @@ export const getAvailableClientRoles = async ({
return await response.json();
};
export const getAvailableClientRoles = async (
query: AvailableClientRolesQuery
): Promise<ClientRole[]> => {
return fetchRoles({ ...query, endpoint: "available-roles" });
};
export const getEffectiveClientRoles = async (
query: EffectiveClientRolesQuery
): Promise<ClientRole[]> => {
return fetchRoles({ ...query, endpoint: "effective-roles" });
};