parent
e9e6b73bd2
commit
8d05895adb
22 changed files with 1378 additions and 0 deletions
6
pom.xml
6
pom.xml
|
@ -278,6 +278,7 @@
|
||||||
<module>themes</module>
|
<module>themes</module>
|
||||||
<module>model</module>
|
<module>model</module>
|
||||||
<module>util</module>
|
<module>util</module>
|
||||||
|
<module>rest</module>
|
||||||
<module>integration</module>
|
<module>integration</module>
|
||||||
<module>adapters</module>
|
<module>adapters</module>
|
||||||
<module>authz</module>
|
<module>authz</module>
|
||||||
|
@ -1450,6 +1451,11 @@
|
||||||
<artifactId>keycloak-admin-ui</artifactId>
|
<artifactId>keycloak-admin-ui</artifactId>
|
||||||
<version>${keycloak.admin-ui.version}</version>
|
<version>${keycloak.admin-ui.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-rest-admin-ui-ext</artifactId>
|
||||||
|
<version>${project.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- Openshift -->
|
<!-- Openshift -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
@ -680,6 +680,10 @@
|
||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-rest-admin-ui-ext</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</profile>
|
</profile>
|
||||||
</profiles>
|
</profiles>
|
||||||
|
|
72
rest/admin-ui-ext/pom.xml
Normal file
72
rest/admin-ui-ext/pom.xml
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
JBoss, Home of Professional Open Source
|
||||||
|
Copyright 2016, Red Hat, Inc. and/or its affiliates, and individual
|
||||||
|
contributors by the @authors tag. See the copyright.txt in the
|
||||||
|
distribution for a full listing of individual contributors.
|
||||||
|
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-rest-parent</artifactId>
|
||||||
|
<version>999-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<artifactId>keycloak-rest-admin-ui-ext</artifactId>
|
||||||
|
<name>Admin UI REST extensions</name>
|
||||||
|
<description>Custom REST endpoints for the Admin UI</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-server-spi</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-server-spi-private</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-services</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.microprofile.openapi</groupId>
|
||||||
|
<artifactId>microprofile-openapi-api</artifactId>
|
||||||
|
<version>3.1</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<artifactId>smallrye-open-api-maven-plugin</artifactId>
|
||||||
|
<groupId>io.smallrye</groupId>
|
||||||
|
<version>3.1.2</version>
|
||||||
|
<configuration>
|
||||||
|
<scanPackages>org.keycloak.admin.ui.rest</scanPackages>
|
||||||
|
</configuration>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>generate-schema</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
|
@ -0,0 +1,40 @@
|
||||||
|
package org.keycloak.admin.ui.rest;
|
||||||
|
|
||||||
|
import org.keycloak.Config;
|
||||||
|
import org.keycloak.common.Profile;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.KeycloakSessionFactory;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.provider.EnvironmentDependentProviderFactory;
|
||||||
|
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 AdminExtProvider implements AdminRealmResourceProviderFactory, AdminRealmResourceProvider, EnvironmentDependentProviderFactory {
|
||||||
|
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 "ui-ext";
|
||||||
|
}
|
||||||
|
|
||||||
|
public Object getResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||||
|
return new AdminExtResource(session, realm, auth, adminEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isSupported() {
|
||||||
|
return Profile.isFeatureEnabled(Profile.Feature.ADMIN2);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package org.keycloak.admin.ui.rest;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.services.resources.admin.AdminEventBuilder;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
|
||||||
|
public final class AdminExtResource {
|
||||||
|
private KeycloakSession session;
|
||||||
|
private RealmModel realm;
|
||||||
|
private AdminPermissionEvaluator auth;
|
||||||
|
private AdminEventBuilder adminEvent;
|
||||||
|
|
||||||
|
public AdminExtResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
|
||||||
|
this.session = session;
|
||||||
|
this.realm = realm;
|
||||||
|
this.auth = auth;
|
||||||
|
this.adminEvent = adminEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/authentication-management")
|
||||||
|
public AuthenticationManagementResource authenticationManagement() {
|
||||||
|
return new AuthenticationManagementResource(session, realm, auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/brute-force-user")
|
||||||
|
public BruteForceUsersResource bruteForceUsers() {
|
||||||
|
return new BruteForceUsersResource(session, realm, auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/available-roles")
|
||||||
|
public AvailableRoleMappingResource availableRoles() {
|
||||||
|
return new AvailableRoleMappingResource(session, realm, auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/effective-roles")
|
||||||
|
public EffectiveRoleMappingResource effectiveRoles() {
|
||||||
|
return new EffectiveRoleMappingResource(session, realm, auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Path("/groups")
|
||||||
|
public GroupsResource groups() {
|
||||||
|
return new GroupsResource(session, realm, auth);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,115 @@
|
||||||
|
package org.keycloak.admin.ui.rest;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.DefaultValue;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
|
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.Authentication;
|
||||||
|
import org.keycloak.admin.ui.rest.model.AuthenticationMapper;
|
||||||
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
|
||||||
|
|
||||||
|
public class AuthenticationManagementResource extends RoleMappingResource {
|
||||||
|
private final KeycloakSession session;
|
||||||
|
|
||||||
|
private RealmModel realm;
|
||||||
|
private AdminPermissionEvaluator auth;
|
||||||
|
|
||||||
|
public AuthenticationManagementResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) {
|
||||||
|
super(realm, auth);
|
||||||
|
this.realm = realm;
|
||||||
|
this.auth = auth;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/flows")
|
||||||
|
@Consumes({"application/json"})
|
||||||
|
@Produces({"application/json"})
|
||||||
|
@Operation(
|
||||||
|
summary = "List all authentication flows for this realm",
|
||||||
|
description = "This endpoint returns all the authentication flows and lists if there they are used."
|
||||||
|
)
|
||||||
|
@APIResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "",
|
||||||
|
content = {@Content(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = Authentication.class,
|
||||||
|
type = SchemaType.ARRAY
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
)
|
||||||
|
public final List<Authentication> listIdentityProviders() {
|
||||||
|
auth.realm().requireViewAuthenticationFlows();
|
||||||
|
|
||||||
|
return realm.getAuthenticationFlowsStream()
|
||||||
|
.filter(flow -> flow.isTopLevel() && !Objects.equals(flow.getAlias(), DefaultAuthenticationFlows.SAML_ECP_FLOW))
|
||||||
|
.map(flow -> AuthenticationMapper.convertToModel(flow, realm))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/{type}/{id}")
|
||||||
|
@Consumes({"application/json"})
|
||||||
|
@Produces({"application/json"})
|
||||||
|
@Operation(
|
||||||
|
summary = "List all clients or identity providers that this flow is used by",
|
||||||
|
description = "List all the clients or identity providers this flow is used by as a paginated list"
|
||||||
|
)
|
||||||
|
@APIResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "",
|
||||||
|
content = {@Content(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = String.class,
|
||||||
|
type = SchemaType.ARRAY
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
)
|
||||||
|
public final List<String> listUsed(@PathParam("id") String id, @PathParam("type") String type, @QueryParam("first") @DefaultValue("0") long first,
|
||||||
|
@QueryParam("max") @DefaultValue("10") long max, @QueryParam("search") @DefaultValue("") String search) {
|
||||||
|
auth.realm().requireViewAuthenticationFlows();
|
||||||
|
|
||||||
|
final AuthenticationFlowModel flow = realm.getAuthenticationFlowsStream().filter(f -> id.equals(f.getId())).collect(Collectors.toList()).get(0);
|
||||||
|
|
||||||
|
if ("clients".equals(type)) {
|
||||||
|
final Stream<ClientModel> clients = realm.getClientsStream();
|
||||||
|
return clients.filter(
|
||||||
|
c -> c.getAuthenticationFlowBindingOverrides().get("browser") != null && c.getAuthenticationFlowBindingOverrides()
|
||||||
|
.get("browser").equals(flow.getId()) || c.getAuthenticationFlowBindingOverrides()
|
||||||
|
.get("direct_grant") != null && c.getAuthenticationFlowBindingOverrides().get("direct_grant").equals(flow.getId()))
|
||||||
|
.map(ClientModel::getClientId).filter(f -> f.contains(search))
|
||||||
|
.skip("".equals(search) ? first : 0).limit(max).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("idp".equals(type)) {
|
||||||
|
final Stream<IdentityProviderModel> identityProviders = realm.getIdentityProvidersStream();
|
||||||
|
return identityProviders.filter(idp -> idp.getFirstBrokerLoginFlowId().equals(flow.getId()))
|
||||||
|
.map(IdentityProviderModel::getAlias).filter(f -> f.contains(search))
|
||||||
|
.skip("".equals(search) ? first : 0).limit(max).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new IllegalArgumentException("Invalid type");
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
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 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 {
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final RealmModel realm;
|
||||||
|
private final AdminPermissionEvaluator auth;
|
||||||
|
|
||||||
|
public AvailableRoleMappingResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) {
|
||||||
|
super(realm, auth);
|
||||||
|
this.realm = realm;
|
||||||
|
this.auth = auth;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,218 @@
|
||||||
|
package org.keycloak.admin.ui.rest;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.DefaultValue;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
|
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.jboss.logging.Logger;
|
||||||
|
import org.keycloak.admin.ui.rest.model.BruteUser;
|
||||||
|
import org.keycloak.common.util.Time;
|
||||||
|
import org.keycloak.models.Constants;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.UserLoginFailureModel;
|
||||||
|
import org.keycloak.models.UserModel;
|
||||||
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.UserPermissionEvaluator;
|
||||||
|
import org.keycloak.utils.SearchQueryUtils;
|
||||||
|
|
||||||
|
public class BruteForceUsersResource {
|
||||||
|
private static final Logger logger = Logger.getLogger(BruteForceUsersResource.class);
|
||||||
|
private static final String SEARCH_ID_PARAMETER = "id:";
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final RealmModel realm;
|
||||||
|
private final AdminPermissionEvaluator auth;
|
||||||
|
|
||||||
|
public BruteForceUsersResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) {
|
||||||
|
this.realm = realm;
|
||||||
|
this.auth = auth;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Consumes({"application/json"})
|
||||||
|
@Produces({"application/json"})
|
||||||
|
@Operation(
|
||||||
|
summary = "Find all users and add if they are locked by brute force protection",
|
||||||
|
description = "Same endpoint as the users search but added brute force protection status."
|
||||||
|
)
|
||||||
|
@APIResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "",
|
||||||
|
content = {@Content(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = BruteUser.class,
|
||||||
|
type = SchemaType.ARRAY
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
)
|
||||||
|
public final Stream<BruteUser> searchUser(@QueryParam("search") String search,
|
||||||
|
@QueryParam("lastName") String last,
|
||||||
|
@QueryParam("firstName") String first,
|
||||||
|
@QueryParam("email") String email,
|
||||||
|
@QueryParam("username") String username,
|
||||||
|
@QueryParam("emailVerified") Boolean emailVerified,
|
||||||
|
@QueryParam("idpAlias") String idpAlias,
|
||||||
|
@QueryParam("idpUserId") String idpUserId,
|
||||||
|
@QueryParam("first") @DefaultValue("-1") Integer firstResult,
|
||||||
|
@QueryParam("max") @DefaultValue("" + Constants.DEFAULT_MAX_RESULTS) Integer maxResults,
|
||||||
|
@QueryParam("enabled") Boolean enabled,
|
||||||
|
@QueryParam("briefRepresentation") Boolean briefRepresentation,
|
||||||
|
@QueryParam("exact") Boolean exact,
|
||||||
|
@QueryParam("q") String searchQuery) {
|
||||||
|
final UserPermissionEvaluator userPermissionEvaluator = auth.users();
|
||||||
|
userPermissionEvaluator.requireQuery();
|
||||||
|
|
||||||
|
Map<String, String> searchAttributes = searchQuery == null
|
||||||
|
? Collections.emptyMap()
|
||||||
|
: SearchQueryUtils.getFields(searchQuery);
|
||||||
|
|
||||||
|
Stream<UserModel> userModels = Stream.empty();
|
||||||
|
if (search != null) {
|
||||||
|
if (search.startsWith(SEARCH_ID_PARAMETER)) {
|
||||||
|
UserModel userModel =
|
||||||
|
session.users().getUserById(realm, search.substring(SEARCH_ID_PARAMETER.length()).trim());
|
||||||
|
if (userModel != null) {
|
||||||
|
userModels = Stream.of(userModel);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Map<String, String> attributes = new HashMap<>();
|
||||||
|
attributes.put(UserModel.SEARCH, search.trim());
|
||||||
|
if (enabled != null) {
|
||||||
|
attributes.put(UserModel.ENABLED, enabled.toString());
|
||||||
|
}
|
||||||
|
return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult,
|
||||||
|
maxResults, false);
|
||||||
|
}
|
||||||
|
} else if (last != null || first != null || email != null || username != null || emailVerified != null
|
||||||
|
|| idpAlias != null || idpUserId != null || enabled != null || exact != null || !searchAttributes.isEmpty()) {
|
||||||
|
Map<String, String> attributes = new HashMap<>();
|
||||||
|
if (last != null) {
|
||||||
|
attributes.put(UserModel.LAST_NAME, last);
|
||||||
|
}
|
||||||
|
if (first != null) {
|
||||||
|
attributes.put(UserModel.FIRST_NAME, first);
|
||||||
|
}
|
||||||
|
if (email != null) {
|
||||||
|
attributes.put(UserModel.EMAIL, email);
|
||||||
|
}
|
||||||
|
if (username != null) {
|
||||||
|
attributes.put(UserModel.USERNAME, username);
|
||||||
|
}
|
||||||
|
if (emailVerified != null) {
|
||||||
|
attributes.put(UserModel.EMAIL_VERIFIED, emailVerified.toString());
|
||||||
|
}
|
||||||
|
if (idpAlias != null) {
|
||||||
|
attributes.put(UserModel.IDP_ALIAS, idpAlias);
|
||||||
|
}
|
||||||
|
if (idpUserId != null) {
|
||||||
|
attributes.put(UserModel.IDP_USER_ID, idpUserId);
|
||||||
|
}
|
||||||
|
if (enabled != null) {
|
||||||
|
attributes.put(UserModel.ENABLED, enabled.toString());
|
||||||
|
}
|
||||||
|
if (exact != null) {
|
||||||
|
attributes.put(UserModel.EXACT, exact.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
attributes.putAll(searchAttributes);
|
||||||
|
|
||||||
|
return searchForUser(attributes, realm, userPermissionEvaluator, briefRepresentation, firstResult,
|
||||||
|
maxResults, true);
|
||||||
|
} else {
|
||||||
|
return searchForUser(new HashMap<>(), realm, userPermissionEvaluator, briefRepresentation,
|
||||||
|
firstResult, maxResults, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return toRepresentation(realm, userPermissionEvaluator, briefRepresentation, userModels);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream<BruteUser> searchForUser(Map<String, String> attributes, RealmModel realm, UserPermissionEvaluator usersEvaluator, Boolean briefRepresentation, Integer firstResult, Integer maxResults, Boolean includeServiceAccounts) {
|
||||||
|
session.setAttribute(UserModel.INCLUDE_SERVICE_ACCOUNT, includeServiceAccounts);
|
||||||
|
|
||||||
|
if (!auth.users().canView()) {
|
||||||
|
Set<String> groupModels = auth.groups().getGroupsWithViewPermission();
|
||||||
|
|
||||||
|
if (!groupModels.isEmpty()) {
|
||||||
|
session.setAttribute(UserModel.GROUPS, groupModels);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<UserModel> userModels = session.users().searchForUserStream(realm, attributes, firstResult, maxResults);
|
||||||
|
return toRepresentation(realm, usersEvaluator, briefRepresentation, userModels);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Stream<BruteUser> toRepresentation(RealmModel realm, UserPermissionEvaluator usersEvaluator,
|
||||||
|
Boolean briefRepresentation, Stream<UserModel> userModels) {
|
||||||
|
boolean briefRepresentationB = briefRepresentation != null && briefRepresentation;
|
||||||
|
boolean canViewGlobal = usersEvaluator.canView();
|
||||||
|
|
||||||
|
usersEvaluator.grantIfNoPermission(session.getAttribute(UserModel.GROUPS) != null);
|
||||||
|
return userModels.filter(user -> canViewGlobal || usersEvaluator.canView(user)).map(user -> {
|
||||||
|
UserRepresentation userRep = briefRepresentationB ?
|
||||||
|
ModelToRepresentation.toBriefRepresentation(user) :
|
||||||
|
ModelToRepresentation.toRepresentation(session, realm, user);
|
||||||
|
userRep.setAccess(usersEvaluator.getAccess(user));
|
||||||
|
return userRep;
|
||||||
|
}).map(this::getBruteForceStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private BruteUser getBruteForceStatus(UserRepresentation user) {
|
||||||
|
BruteUser bruteUser = new BruteUser(user);
|
||||||
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("disabled", false);
|
||||||
|
data.put("numFailures", 0);
|
||||||
|
data.put("lastFailure", 0);
|
||||||
|
data.put("lastIPFailure", "n/a");
|
||||||
|
if (!realm.isBruteForceProtected())
|
||||||
|
bruteUser.setBruteForceStatus(data);
|
||||||
|
|
||||||
|
UserLoginFailureModel model = session.loginFailures().getUserLoginFailure(realm, user.getId());
|
||||||
|
if (model == null) {
|
||||||
|
bruteUser.setBruteForceStatus(data);
|
||||||
|
return bruteUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean disabled;
|
||||||
|
disabled = isTemporarilyDisabled(session, realm, user);
|
||||||
|
if (disabled) {
|
||||||
|
data.put("disabled", true);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.put("numFailures", model.getNumFailures());
|
||||||
|
data.put("lastFailure", model.getLastFailure());
|
||||||
|
data.put("lastIPFailure", model.getLastIPFailure());
|
||||||
|
bruteUser.setBruteForceStatus(data);
|
||||||
|
|
||||||
|
return bruteUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTemporarilyDisabled(KeycloakSession session, RealmModel realm, UserRepresentation user) {
|
||||||
|
UserLoginFailureModel failure = session.loginFailures().getUserLoginFailure(realm, user.getId());
|
||||||
|
if (failure != null) {
|
||||||
|
int currTime = (int)(Time.currentTimeMillis() / 1000L);
|
||||||
|
int failedLoginNotBefore = failure.getFailedLoginNotBefore();
|
||||||
|
if (currTime < failedLoginNotBefore) {
|
||||||
|
logger.debugv("Current: {0} notBefore: {1}", currTime, failedLoginNotBefore);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,170 @@
|
||||||
|
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 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 {
|
||||||
|
private KeycloakSession session;
|
||||||
|
private RealmModel realm;
|
||||||
|
private AdminPermissionEvaluator auth;
|
||||||
|
|
||||||
|
public EffectiveRoleMappingResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) {
|
||||||
|
super(realm, auth);
|
||||||
|
this.realm = realm;
|
||||||
|
this.auth = auth;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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::hasRole).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::hasRole).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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package org.keycloak.admin.ui.rest;
|
||||||
|
|
||||||
|
import static org.keycloak.models.utils.ModelToRepresentation.toRepresentation;
|
||||||
|
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import javax.ws.rs.Consumes;
|
||||||
|
import javax.ws.rs.DefaultValue;
|
||||||
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.Produces;
|
||||||
|
import javax.ws.rs.QueryParam;
|
||||||
|
|
||||||
|
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.models.GroupModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
import org.keycloak.models.utils.ModelToRepresentation;
|
||||||
|
import org.keycloak.representations.idm.GroupRepresentation;
|
||||||
|
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
|
||||||
|
import org.keycloak.utils.StringUtil;
|
||||||
|
|
||||||
|
public class GroupsResource {
|
||||||
|
private final KeycloakSession session;
|
||||||
|
private final RealmModel realm;
|
||||||
|
private final AdminPermissionEvaluator auth;
|
||||||
|
|
||||||
|
public GroupsResource(KeycloakSession session, RealmModel realm, AdminPermissionEvaluator auth) {
|
||||||
|
super();
|
||||||
|
this.realm = realm;
|
||||||
|
this.auth = auth;
|
||||||
|
this.session = session;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Consumes({"application/json"})
|
||||||
|
@Produces({"application/json"})
|
||||||
|
@Operation(
|
||||||
|
summary = "List all groups with fine grained authorisation",
|
||||||
|
description = "This endpoint returns a list of groups with fine grained authorisation"
|
||||||
|
)
|
||||||
|
@APIResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "",
|
||||||
|
content = {@Content(
|
||||||
|
schema = @Schema(
|
||||||
|
implementation = GroupRepresentation.class,
|
||||||
|
type = SchemaType.ARRAY
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
)
|
||||||
|
public final Stream<GroupRepresentation> listGroups(@QueryParam("search") @DefaultValue("") final String search, @QueryParam("first")
|
||||||
|
@DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max, @QueryParam("global") @DefaultValue("true") boolean global,
|
||||||
|
@QueryParam("exact") @DefaultValue("false") boolean exact) {
|
||||||
|
this.auth.groups().requireList();
|
||||||
|
final Stream<GroupModel> stream;
|
||||||
|
if (!"".equals(search)) {
|
||||||
|
if (global) {
|
||||||
|
stream = session.groups().searchForGroupByNameStream(realm, search, exact, first, max);
|
||||||
|
} else {
|
||||||
|
stream = this.realm.getTopLevelGroupsStream().filter(g -> g.getName().contains(search)).skip(first).limit(max);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
stream = this.realm.getTopLevelGroupsStream(first, max);
|
||||||
|
}
|
||||||
|
return stream.map(g -> toGroupHierarchy(g, search, exact));
|
||||||
|
}
|
||||||
|
|
||||||
|
private GroupRepresentation toGroupHierarchy(GroupModel group, final String search, boolean exact) {
|
||||||
|
GroupRepresentation rep = toRepresentation(group, true);
|
||||||
|
rep.setAccess(auth.groups().getAccess(group));
|
||||||
|
rep.setSubGroups(group.getSubGroupsStream().filter(g ->
|
||||||
|
groupMatchesSearchOrIsPathElement(
|
||||||
|
g, search
|
||||||
|
)
|
||||||
|
).map(subGroup ->
|
||||||
|
ModelToRepresentation.toGroupHierarchy(
|
||||||
|
subGroup, true, search, exact
|
||||||
|
)
|
||||||
|
).collect(Collectors.toList()));
|
||||||
|
|
||||||
|
return rep;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean groupMatchesSearchOrIsPathElement(GroupModel group, String search) {
|
||||||
|
if (StringUtil.isBlank(search)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (group.getName().contains(search)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return group.getSubGroupsStream().findAny().isPresent();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package org.keycloak.admin.ui.rest;
|
||||||
|
|
||||||
|
import static org.keycloak.admin.ui.rest.model.RoleMapper.convertToModel;
|
||||||
|
|
||||||
|
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 final RealmModel realm;
|
||||||
|
private final AdminPermissionEvaluator auth;
|
||||||
|
|
||||||
|
public RoleMappingResource(RealmModel realm, AdminPermissionEvaluator auth) {
|
||||||
|
this.realm = realm;
|
||||||
|
this.auth = auth;
|
||||||
|
}
|
||||||
|
|
||||||
|
public final Stream<ClientRole> mapping(Predicate<RoleModel> predicate) {
|
||||||
|
return realm.getClientsStream().flatMap(RoleContainerModel::getRolesStream).filter(predicate)
|
||||||
|
.filter(auth.roles()::canMapClientScope).map(roleModel -> convertToModel(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(first).limit(max).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,80 @@
|
||||||
|
package org.keycloak.admin.ui.rest.model;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import org.eclipse.microprofile.openapi.annotations.media.Schema;
|
||||||
|
|
||||||
|
public class Authentication {
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private String alias;
|
||||||
|
|
||||||
|
@Schema(required = true)
|
||||||
|
private boolean builtIn;
|
||||||
|
|
||||||
|
private UsedBy usedBy;
|
||||||
|
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
public UsedBy getUsedBy() {
|
||||||
|
return usedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsedBy( UsedBy usedBy) {
|
||||||
|
this.usedBy = usedBy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(String id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBuiltIn() {
|
||||||
|
return builtIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBuiltIn(boolean builtIn) {
|
||||||
|
this.builtIn = builtIn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAlias() {
|
||||||
|
return alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAlias(String alias) {
|
||||||
|
this.alias = alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDescription() {
|
||||||
|
return description;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDescription(String description) {
|
||||||
|
this.description = description;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o)
|
||||||
|
return true;
|
||||||
|
if (o == null || getClass() != o.getClass())
|
||||||
|
return false;
|
||||||
|
Authentication that = (Authentication) o;
|
||||||
|
return builtIn == that.builtIn && Objects.equals(usedBy, that.usedBy) && Objects.equals(id, that.id) && Objects.equals(alias,
|
||||||
|
that.alias) && Objects.equals(description, that.description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(usedBy, id, builtIn, alias, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public String toString() {
|
||||||
|
return "Authentication{" + "usedBy=" + usedBy + ", id='" + id + '\'' + ", buildIn=" + builtIn + ", alias='" + alias + '\'' + ", description='" + description + '\'' + '}';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,51 @@
|
||||||
|
package org.keycloak.admin.ui.rest.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
|
import org.keycloak.models.ClientModel;
|
||||||
|
import org.keycloak.models.IdentityProviderModel;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
|
|
||||||
|
public class AuthenticationMapper {
|
||||||
|
private static final int MAX_USED_BY = 9;
|
||||||
|
|
||||||
|
public static Authentication convertToModel(AuthenticationFlowModel flow, RealmModel realm) {
|
||||||
|
|
||||||
|
final Stream<IdentityProviderModel> identityProviders = realm.getIdentityProvidersStream();
|
||||||
|
final Stream<ClientModel> clients = realm.getClientsStream();
|
||||||
|
|
||||||
|
final Authentication authentication = new Authentication();
|
||||||
|
authentication.setId(flow.getId());
|
||||||
|
authentication.setAlias(flow.getAlias());
|
||||||
|
authentication.setBuiltIn(flow.isBuiltIn());
|
||||||
|
authentication.setDescription(flow.getDescription());
|
||||||
|
|
||||||
|
final List<String> usedByIdp = identityProviders.filter(idp -> idp.getFirstBrokerLoginFlowId().equals(flow.getId()))
|
||||||
|
.map(IdentityProviderModel::getAlias).limit(MAX_USED_BY).collect(Collectors.toList());
|
||||||
|
if (!usedByIdp.isEmpty()) {
|
||||||
|
authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.SPECIFIC_PROVIDERS, usedByIdp));
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> usedClients = clients.filter(
|
||||||
|
c -> c.getAuthenticationFlowBindingOverrides().get("browser") != null && c.getAuthenticationFlowBindingOverrides()
|
||||||
|
.get("browser").equals(flow.getId()) || c.getAuthenticationFlowBindingOverrides()
|
||||||
|
.get("direct_grant") != null && c.getAuthenticationFlowBindingOverrides().get("direct_grant").equals(flow.getId()))
|
||||||
|
.map(ClientModel::getClientId).limit(MAX_USED_BY).collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!usedClients.isEmpty()) {
|
||||||
|
authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.SPECIFIC_CLIENTS, usedClients));
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<String> useAsDefault = Stream.of(realm.getBrowserFlow(), realm.getRegistrationFlow(), realm.getDirectGrantFlow(),
|
||||||
|
realm.getResetCredentialsFlow(), realm.getClientAuthenticationFlow(), realm.getDockerAuthenticationFlow())
|
||||||
|
.filter(f -> flow.getAlias().equals(f.getAlias())).map(AuthenticationFlowModel::getAlias).collect(Collectors.toList());
|
||||||
|
|
||||||
|
if (!useAsDefault.isEmpty()) {
|
||||||
|
authentication.setUsedBy(new UsedBy(UsedBy.UsedByType.DEFAULT, useAsDefault));
|
||||||
|
}
|
||||||
|
|
||||||
|
return authentication;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
package org.keycloak.admin.ui.rest.model;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
|
||||||
|
public class BruteUser extends UserRepresentation {
|
||||||
|
|
||||||
|
Map<String, Object> bruteForceStatus;
|
||||||
|
|
||||||
|
public BruteUser(UserRepresentation user) {
|
||||||
|
this.id = user.getId();
|
||||||
|
this.origin = user.getOrigin();
|
||||||
|
this.createdTimestamp = user.getCreatedTimestamp();
|
||||||
|
this.username = user.getUsername();
|
||||||
|
this.enabled = user.isEnabled();
|
||||||
|
this.totp = user.isTotp();
|
||||||
|
this.emailVerified = user.isEmailVerified();
|
||||||
|
this.firstName = user.getFirstName();
|
||||||
|
this.lastName = user.getLastName();
|
||||||
|
this.email = user.getEmail();
|
||||||
|
this.federationLink = user.getFederationLink();
|
||||||
|
this.serviceAccountClientId = user.getServiceAccountClientId();
|
||||||
|
|
||||||
|
this.attributes = user.getAttributes();
|
||||||
|
this.credentials = user.getCredentials();
|
||||||
|
this.disableableCredentialTypes = user.getDisableableCredentialTypes();
|
||||||
|
this.requiredActions = user.getRequiredActions();
|
||||||
|
this.federatedIdentities = user.getFederatedIdentities();
|
||||||
|
this.realmRoles = user.getRealmRoles();
|
||||||
|
this.clientRoles = user.getClientRoles();
|
||||||
|
this.clientConsents = user.getClientConsents();
|
||||||
|
this.notBefore = user.getNotBefore();
|
||||||
|
|
||||||
|
this.applicationRoles = user.getApplicationRoles();
|
||||||
|
this.socialLinks = user.getSocialLinks();
|
||||||
|
|
||||||
|
this.groups = user.getGroups();
|
||||||
|
this.setAccess(user.getAccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getBruteForceStatus() {
|
||||||
|
return bruteForceStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBruteForceStatus(Map<String, Object> bruteForceStatus) {
|
||||||
|
this.bruteForceStatus = bruteForceStatus;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 convertToModel(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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package org.keycloak.admin.ui.rest.model;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
public class UsedBy {
|
||||||
|
public UsedBy(UsedByType type, List<String> values) {
|
||||||
|
this.type = type;
|
||||||
|
this.values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum UsedByType {
|
||||||
|
SPECIFIC_CLIENTS, SPECIFIC_PROVIDERS, DEFAULT
|
||||||
|
}
|
||||||
|
|
||||||
|
private UsedByType type;
|
||||||
|
private List<String> values;
|
||||||
|
|
||||||
|
public UsedByType getType() {
|
||||||
|
return type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setType(UsedByType type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getValues() {
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setValues(List<String> values) {
|
||||||
|
this.values = values;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o)
|
||||||
|
return true;
|
||||||
|
if (o == null || getClass() != o.getClass())
|
||||||
|
return false;
|
||||||
|
UsedBy usedBy = (UsedBy) o;
|
||||||
|
return type == usedBy.type && Objects.equals(values, usedBy.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(type, values);
|
||||||
|
}
|
||||||
|
}
|
0
rest/admin-ui-ext/src/main/resources/META-INF/beans.xml
Normal file
0
rest/admin-ui-ext/src/main/resources/META-INF/beans.xml
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
#
|
||||||
|
# Copyright 2022 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.
|
||||||
|
#
|
||||||
|
|
||||||
|
org.keycloak.admin.ui.rest.AdminExtProvider
|
38
rest/pom.xml
Normal file
38
rest/pom.xml
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
JBoss, Home of Professional Open Source
|
||||||
|
Copyright 2016, Red Hat, Inc. and/or its affiliates, and individual
|
||||||
|
contributors by the @authors tag. See the copyright.txt in the
|
||||||
|
distribution for a full listing of individual contributors.
|
||||||
|
|
||||||
|
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.
|
||||||
|
-->
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<artifactId>keycloak-parent</artifactId>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<version>999-SNAPSHOT</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<name>Keycloak Administration UI</name>
|
||||||
|
<description>Keycloak Administration UI</description>
|
||||||
|
|
||||||
|
<artifactId>keycloak-rest-parent</artifactId>
|
||||||
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
<modules>
|
||||||
|
<module>admin-ui-ext</module>
|
||||||
|
</modules>
|
||||||
|
|
||||||
|
</project>
|
|
@ -223,6 +223,10 @@
|
||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-rest-admin-ui-ext</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
|
|
|
@ -64,6 +64,10 @@
|
||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.keycloak</groupId>
|
||||||
|
<artifactId>keycloak-rest-admin-ui-ext</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.keycloak</groupId>
|
<groupId>org.keycloak</groupId>
|
||||||
<artifactId>keycloak-admin-client</artifactId>
|
<artifactId>keycloak-admin-client</artifactId>
|
||||||
|
|
Loading…
Reference in a new issue