parent
b5ea27c194
commit
a0a154d9c9
15 changed files with 556 additions and 562 deletions
|
@ -37,8 +37,6 @@
|
|||
<frontend.maven.plugin.nodeVersion>v18.7.0</frontend.maven.plugin.nodeVersion>
|
||||
|
||||
<keycloak.version>19.0.1</keycloak.version>
|
||||
<kotlin.version>1.7.10</kotlin.version>
|
||||
<mapstruct.version>1.5.2.Final</mapstruct.version>
|
||||
</properties>
|
||||
|
||||
<licenses>
|
||||
|
@ -93,27 +91,11 @@
|
|||
<artifactId>keycloak-services</artifactId>
|
||||
<version>${keycloak.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.eclipse.microprofile.openapi</groupId>
|
||||
<artifactId>microprofile-openapi-api</artifactId>
|
||||
<version>3.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-test</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testng</groupId>
|
||||
<artifactId>testng</artifactId>
|
||||
|
@ -191,78 +173,10 @@
|
|||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>kapt</id>
|
||||
<goals>
|
||||
<goal>kapt</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirs>
|
||||
<sourceDir>src/main/java</sourceDir>
|
||||
</sourceDirs>
|
||||
<annotationProcessorPaths>
|
||||
<annotationProcessorPath>
|
||||
<groupId>org.mapstruct</groupId>
|
||||
<artifactId>mapstruct-processor</artifactId>
|
||||
<version>${mapstruct.version}</version>
|
||||
</annotationProcessorPath>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<jvmTarget>1.8</jvmTarget>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.10.1</version>
|
||||
<executions>
|
||||
<!-- Replacing default-compile as it is treated specially by maven -->
|
||||
<execution>
|
||||
<id>default-compile</id>
|
||||
<phase>none</phase>
|
||||
</execution>
|
||||
<!-- Replacing default-testCompile as it is treated specially by maven -->
|
||||
<execution>
|
||||
<id>default-testCompile</id>
|
||||
<phase>none</phase>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>java-compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>java-test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>testCompile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<source>8</source>
|
||||
<target>8</target>
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
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;
|
||||
|
||||
public final class AvailableRoleMappingProvider implements AdminRealmResourceProviderFactory, AdminRealmResourceProvider {
|
||||
public AdminRealmResourceProvider create(KeycloakSession session) {
|
||||
return this;
|
||||
}
|
||||
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
public void close() {
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return "admin-ui-available-roles";
|
||||
}
|
||||
|
||||
public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||
return new AvailableRoleMappingResource(realm, auth);
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
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 AvailableRoleMappingProvider : 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-available-roles"
|
||||
}
|
||||
|
||||
override fun getResource(
|
||||
session: KeycloakSession,
|
||||
realm: RealmModel,
|
||||
auth: AdminPermissionEvaluator,
|
||||
adminEvent: AdminEventBuilder
|
||||
): Any {
|
||||
return AvailableRoleMappingResource(realm, auth)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,184 @@
|
|||
package org.keycloak.admin.ui.rest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Predicate;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DefaultValue;
|
||||
import javax.ws.rs.ForbiddenException;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.QueryParam;
|
||||
import javax.ws.rs.core.Context;
|
||||
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.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserProvider;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
|
||||
public class AvailableRoleMappingResource extends RoleMappingResource {
|
||||
@Context
|
||||
private KeycloakSession session;
|
||||
private final RealmModel realm;
|
||||
private final AdminPermissionEvaluator auth;
|
||||
|
||||
public AvailableRoleMappingResource(RealmModel realm, AdminPermissionEvaluator auth) {
|
||||
super(realm, auth);
|
||||
this.realm = realm;
|
||||
this.auth = auth;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/clientScopes/{id}")
|
||||
@Consumes({"application/json"})
|
||||
@Produces({"application/json"})
|
||||
@Operation(
|
||||
summary = "List all composite client 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(
|
||||
implementation = ClientRole.class,
|
||||
type = SchemaType.ARRAY
|
||||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> listCompositeClientScopeRoleMappings(@PathParam("id") String id, @QueryParam("first")
|
||||
@DefaultValue("0") long first, @QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) {
|
||||
ClientScopeModel scopeModel = this.realm.getClientScopeById(id);
|
||||
if (scopeModel == null) {
|
||||
throw new NotFoundException("Could not find client scope");
|
||||
} else {
|
||||
this.auth.clients().requireView(scopeModel);
|
||||
return this.mapping(((Predicate<RoleModel>) scopeModel::hasDirectScope).negate(), first, max, search);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/clients/{id}")
|
||||
@Consumes({"application/json"})
|
||||
@Produces({"application/json"})
|
||||
@Operation(
|
||||
summary = "List all composite client 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(
|
||||
implementation = ClientRole.class,
|
||||
type = SchemaType.ARRAY
|
||||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> listCompositeClientRoleMappings(@PathParam("id") String id, @QueryParam("first")
|
||||
@DefaultValue("0") long first, @QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) {
|
||||
ClientModel client = this.realm.getClientById(id);
|
||||
if (client == null) {
|
||||
throw new NotFoundException("Could not find client");
|
||||
} else {
|
||||
this.auth.clients().requireView(client);
|
||||
return this.mapping(((Predicate<RoleModel>) client::hasDirectScope).negate(), first, max, search);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/groups/{id}")
|
||||
@Consumes({"application/json"})
|
||||
@Produces({"application/json"})
|
||||
@Operation(
|
||||
summary = "List all composite client 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(
|
||||
implementation = ClientRole.class,
|
||||
type = SchemaType.ARRAY
|
||||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> listCompositeGroupRoleMappings(@PathParam("id") String id, @QueryParam("first")
|
||||
@DefaultValue("0") long first, @QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) {
|
||||
GroupModel group = this.realm.getGroupById(id);
|
||||
if (group == null) {
|
||||
throw new NotFoundException("Could not find group");
|
||||
} else {
|
||||
this.auth.groups().requireView(group);
|
||||
return this.mapping(((Predicate<RoleModel>) group::hasDirectRole).negate(), first, max, search);
|
||||
}
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/users/{id}")
|
||||
@Consumes({"application/json"})
|
||||
@Produces({"application/json"})
|
||||
@Operation(
|
||||
summary = "List all composite client roles for this user",
|
||||
description = "This endpoint returns all the client role mapping for a specific user"
|
||||
)
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
description = "",
|
||||
content = {@Content(
|
||||
schema = @Schema(
|
||||
implementation = ClientRole.class,
|
||||
type = SchemaType.ARRAY
|
||||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> listCompositeUserRoleMappings(@PathParam("id") String id, @QueryParam("first") @DefaultValue("0") long first,
|
||||
@QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) {
|
||||
UserProvider users = Objects.requireNonNull(session).users();
|
||||
UserModel userModel = users.getUserById(this.realm, id);
|
||||
if (userModel == null) {
|
||||
if (auth.users().canQuery()) throw new NotFoundException("User not found");
|
||||
else throw new ForbiddenException();
|
||||
}
|
||||
|
||||
this.auth.users().requireView(userModel);
|
||||
return this.mapping(((Predicate<RoleModel>) userModel::hasDirectRole).negate(), first, max, search);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/roles/{id}")
|
||||
@Consumes({"application/json"})
|
||||
@Produces({"application/json"})
|
||||
@Operation(
|
||||
summary = "List all composite client roles",
|
||||
description = "This endpoint returns all the client role"
|
||||
)
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
description = "",
|
||||
content = {@Content(
|
||||
schema = @Schema(
|
||||
implementation = ClientRole.class,
|
||||
type = SchemaType.ARRAY
|
||||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> listCompositeRoleMappings(@QueryParam("first") @DefaultValue("0") long first,
|
||||
@QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) {
|
||||
return this.mapping(o -> true, first, max, search);
|
||||
}
|
||||
}
|
|
@ -1,178 +0,0 @@
|
|||
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.admin.ui.rest.model.RoleMapper
|
||||
import org.keycloak.models.KeycloakSession
|
||||
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 javax.ws.rs.*
|
||||
import javax.ws.rs.core.Context
|
||||
import javax.ws.rs.core.MediaType
|
||||
|
||||
@Path("/")
|
||||
open class AvailableRoleMappingResource(
|
||||
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 composite client 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,
|
||||
@QueryParam("first") @DefaultValue("0") first: Long,
|
||||
@QueryParam("max") @DefaultValue("10") max: Long,
|
||||
@QueryParam("search") @DefaultValue("") search: String
|
||||
): List<ClientRole> {
|
||||
val scopeContainer = realm.getClientScopeById(id) ?: throw NotFoundException("Could not find client scope")
|
||||
auth.clients().requireView(scopeContainer)
|
||||
|
||||
return mapping(Predicate<RoleModel?> { r -> scopeContainer.hasDirectScope(r) }.negate(), first, max, search)
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/clients/{id}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(
|
||||
summary = "List all composite client 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 listCompositeClientRoleMappings(
|
||||
@PathParam("id") id: String,
|
||||
@QueryParam("first") @DefaultValue("0") first: Long,
|
||||
@QueryParam("max") @DefaultValue("10") max: Long,
|
||||
@QueryParam("search") @DefaultValue("") search: String
|
||||
): List<ClientRole> {
|
||||
val scopeContainer = realm.getClientById(id) ?: throw NotFoundException("Could not find client")
|
||||
auth.clients().requireView(scopeContainer)
|
||||
|
||||
return mapping(Predicate<RoleModel?> { r -> scopeContainer.hasDirectScope(r) }.negate(), first, max, search)
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/groups/{id}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(
|
||||
summary = "List all composite client 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 listCompositeGroupRoleMappings(
|
||||
@PathParam("id") id: String,
|
||||
@QueryParam("first") @DefaultValue("0") first: Long,
|
||||
@QueryParam("max") @DefaultValue("10") max: Long,
|
||||
@QueryParam("search") @DefaultValue("") search: String
|
||||
): List<ClientRole> {
|
||||
val scopeContainer = realm.getGroupById(id) ?: throw NotFoundException("Could not find group")
|
||||
auth.groups().requireView(scopeContainer)
|
||||
|
||||
return mapping(Predicate<RoleModel?> { r -> scopeContainer.hasDirectRole(r) }.negate(), first, max, search)
|
||||
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/users/{id}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(
|
||||
summary = "List all composite client roles for this user",
|
||||
description = "This endpoint returns all the client role mapping for a specific user"
|
||||
)
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
description = "",
|
||||
content = [Content(
|
||||
schema = Schema(
|
||||
type = SchemaType.ARRAY,
|
||||
implementation = ClientRole::class
|
||||
)
|
||||
)]
|
||||
)
|
||||
fun listCompositeUserRoleMappings(
|
||||
@PathParam("id") id: String,
|
||||
@QueryParam("first") @DefaultValue("0") first: Long,
|
||||
@QueryParam("max") @DefaultValue("10") max: Long,
|
||||
@QueryParam("search") @DefaultValue("") search: 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(Predicate<RoleModel?> { r -> user.hasDirectRole(r) }.negate(), first, max, search)
|
||||
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/roles/{id}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Operation(
|
||||
summary = "List all composite client roles",
|
||||
description = "This endpoint returns all the client role"
|
||||
)
|
||||
@APIResponse(
|
||||
responseCode = "200",
|
||||
description = "",
|
||||
content = [Content(
|
||||
schema = Schema(
|
||||
type = SchemaType.ARRAY,
|
||||
implementation = ClientRole::class
|
||||
)
|
||||
)]
|
||||
)
|
||||
fun listCompositeRoleMappings(
|
||||
@QueryParam("first") @DefaultValue("0") first: Long,
|
||||
@QueryParam("max") @DefaultValue("10") max: Long,
|
||||
@QueryParam("search") @DefaultValue("") search: String
|
||||
): List<ClientRole> {
|
||||
return mapping({ true }, first, max, search)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
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;
|
||||
|
||||
public final class EffectiveRoleMappingProvider implements AdminRealmResourceProviderFactory, AdminRealmResourceProvider {
|
||||
public AdminRealmResourceProvider create(KeycloakSession session) {
|
||||
return this;
|
||||
}
|
||||
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
public void close() {
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return "admin-ui-effective-roles";
|
||||
}
|
||||
|
||||
public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||
return new EffectiveRoleMappingResource(realm, auth);
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
package org.keycloak.admin.ui.rest;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.ForbiddenException;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.NotFoundException;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
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.ClientModel;
|
||||
import org.keycloak.models.ClientScopeModel;
|
||||
import org.keycloak.models.GroupModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
|
||||
public class EffectiveRoleMappingResource extends RoleMappingResource {
|
||||
@Context
|
||||
private KeycloakSession session;
|
||||
private RealmModel realm;
|
||||
private AdminPermissionEvaluator auth;
|
||||
|
||||
public EffectiveRoleMappingResource(RealmModel realm, AdminPermissionEvaluator auth) {
|
||||
super(realm, auth);
|
||||
this.realm = realm;
|
||||
this.auth = auth;
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/clientScopes/{id}")
|
||||
@Consumes({"application/json"})
|
||||
@Produces({"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(
|
||||
implementation = ClientRole.class,
|
||||
type = SchemaType.ARRAY
|
||||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> listCompositeClientScopeRoleMappings(@PathParam("id") String id) {
|
||||
ClientScopeModel clientScope = this.realm.getClientScopeById(id);
|
||||
if (clientScope == null) {
|
||||
throw new NotFoundException("Could not find client scope");
|
||||
}
|
||||
|
||||
this.auth.clients().requireView(clientScope);
|
||||
return this.mapping(clientScope::hasScope).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/clients/{id}")
|
||||
@Consumes({"application/json"})
|
||||
@Produces({"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(
|
||||
implementation = ClientRole.class,
|
||||
type = SchemaType.ARRAY
|
||||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> listCompositeClientsRoleMappings(@PathParam("id") String id) {
|
||||
ClientModel client = this.realm.getClientById(id);
|
||||
if (client == null) {
|
||||
throw new NotFoundException("Could not find client");
|
||||
}
|
||||
auth.clients().requireView(client);
|
||||
return mapping(client::hasScope).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/groups/{id}")
|
||||
@Consumes({"application/json"})
|
||||
@Produces({"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(
|
||||
implementation = ClientRole.class,
|
||||
type = SchemaType.ARRAY
|
||||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> listCompositeGroupsRoleMappings(@PathParam("id") String id) {
|
||||
GroupModel group = this.realm.getGroupById(id);
|
||||
if (group == null) {
|
||||
throw new NotFoundException("Could not find group");
|
||||
}
|
||||
|
||||
return mapping(group::hasDirectRole).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/users/{id}")
|
||||
@Consumes({"application/json"})
|
||||
@Produces({"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(
|
||||
implementation = ClientRole.class,
|
||||
type = SchemaType.ARRAY
|
||||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> listCompositeUsersRoleMappings(@PathParam("id") String id) {
|
||||
UserModel user = session.users().getUserById(this.realm, id);
|
||||
if (user == null) {
|
||||
if (auth.users().canQuery()) throw new NotFoundException("User not found");
|
||||
else throw new ForbiddenException();
|
||||
}
|
||||
|
||||
return mapping(user::hasDirectRole).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GET
|
||||
@Path("/roles/{id}")
|
||||
@Consumes({"application/json"})
|
||||
@Produces({"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(
|
||||
implementation = ClientRole.class,
|
||||
type = SchemaType.ARRAY
|
||||
)
|
||||
)}
|
||||
)
|
||||
public final List<ClientRole> listCompositeRealmRoleMappings() {
|
||||
return mapping (o -> true ).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
|
@ -1,156 +0,0 @@
|
|||
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())
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package org.keycloak.admin.ui.rest;
|
||||
|
||||
import static org.keycloak.admin.ui.rest.model.RoleMapper.convertToRepresentation;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import org.keycloak.admin.ui.rest.model.ClientRole;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleContainerModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||
|
||||
public abstract class RoleMappingResource {
|
||||
private RealmModel realm;
|
||||
private AdminPermissionEvaluator auth;
|
||||
|
||||
public final Stream<ClientRole> mapping(Predicate<RoleModel> predicate) {
|
||||
return realm.getClientsStream().flatMap(RoleContainerModel::getRolesStream).filter(predicate)
|
||||
.filter(auth.roles()::canMapClientScope).map(roleModel -> convertToRepresentation(roleModel, realm.getClientsStream()));
|
||||
}
|
||||
|
||||
public final List<ClientRole> mapping(Predicate<RoleModel> predicate, long first, long max, final String search) {
|
||||
|
||||
return mapping(predicate).filter(clientRole -> clientRole.getClient().contains(search) || clientRole.getRole().contains(search))
|
||||
|
||||
.skip("".equals(search) ? first : 0).limit(max).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public RoleMappingResource(RealmModel realm, AdminPermissionEvaluator auth) {
|
||||
this.realm = realm;
|
||||
this.auth = auth;
|
||||
}
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
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()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
package org.keycloak.admin.ui.rest.model;
|
||||
|
||||
import java.util.Objects;
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||
|
||||
public final class ClientRole {
|
||||
@Schema(required = true) private final String id;
|
||||
@Schema(required = true) private final String role;
|
||||
@Schema(required = true) private String client;
|
||||
@Schema(required = true) private String clientId;
|
||||
private String description;
|
||||
|
||||
public String getId() {
|
||||
return this.id;
|
||||
}
|
||||
|
||||
public String getRole() {
|
||||
return this.role;
|
||||
}
|
||||
|
||||
public String getClient() {
|
||||
return this.client;
|
||||
}
|
||||
|
||||
public void setClient(String client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
public String getClientId() {
|
||||
return this.clientId;
|
||||
}
|
||||
|
||||
public void setClientId(String clientId) {
|
||||
this.clientId = clientId;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public ClientRole(String id, String role, String description) {
|
||||
this.id = id;
|
||||
this.role = role;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public ClientRole(String id, String role, String client, String clientId, String description) {
|
||||
this.id = id;
|
||||
this.role = role;
|
||||
this.client = client;
|
||||
this.clientId = clientId;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public ClientRole copy(String id, String role, String client, String clientId, String description) {
|
||||
return new ClientRole(id, role, client, clientId, description);
|
||||
}
|
||||
|
||||
@Override public String toString() {
|
||||
return "ClientRole{" + "id='" + id + '\'' + ", role='" + role + '\'' + ", client='" + client + '\'' + ", clientId='" + clientId + '\'' + ", description='" + description + '\'' + '}';
|
||||
}
|
||||
|
||||
@Override public boolean equals(Object o) {
|
||||
if (this == o)
|
||||
return true;
|
||||
if (o == null || getClass() != o.getClass())
|
||||
return false;
|
||||
ClientRole that = (ClientRole) o;
|
||||
return id.equals(that.id) && role.equals(that.role) && client.equals(that.client) && clientId.equals(that.clientId);
|
||||
}
|
||||
|
||||
@Override public int hashCode() {
|
||||
return Objects.hash(id, role, client, clientId);
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package org.keycloak.admin.ui.rest.model
|
||||
|
||||
import org.eclipse.microprofile.openapi.annotations.media.Schema
|
||||
|
||||
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?
|
||||
) {
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package org.keycloak.admin.ui.rest.model;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
|
||||
public class RoleMapper {
|
||||
|
||||
public static ClientRole convertToRepresentation(RoleModel roleModel, Stream<ClientModel> clients) {
|
||||
ClientRole clientRole = new ClientRole(roleModel.getId(), roleModel.getName(), roleModel.getDescription());
|
||||
ClientModel clientModel = clients.filter(c -> roleModel.getContainerId().equals(c.getId())).findFirst()
|
||||
.orElseThrow(() -> new IllegalArgumentException("Could not find referenced client"));
|
||||
clientRole.setClientId(clientModel.getId());
|
||||
clientRole.setClient(clientModel.getClientId());
|
||||
return clientRole;
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
package org.keycloak.admin.ui.rest.model
|
||||
|
||||
import org.keycloak.models.ClientModel
|
||||
import org.keycloak.models.RoleModel
|
||||
import org.mapstruct.*
|
||||
import java.util.stream.Stream
|
||||
|
||||
@Mapper
|
||||
abstract class RoleMapper {
|
||||
|
||||
@Mapping(source = "name", target = "role")
|
||||
@Mapping(target = "client", ignore = true)
|
||||
abstract fun convertToRepresentation(role: RoleModel, @Context clientModel: Stream<ClientModel>): ClientRole
|
||||
|
||||
@AfterMapping
|
||||
fun convert(role: RoleModel, @MappingTarget clientRole: ClientRole, @Context list: Stream<ClientModel>) {
|
||||
val clientModel = list.filter { c -> role.containerId == c.id }.findFirst().get()
|
||||
clientRole.clientId = clientModel.id
|
||||
clientRole.client = clientModel.clientId
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue