Move Admin REST extension to main repository (#16530)

Closes #16529
This commit is contained in:
Stian Thorgersen 2023-01-19 13:06:21 +01:00 committed by GitHub
parent e9e6b73bd2
commit 8d05895adb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 1378 additions and 0 deletions

View file

@ -278,6 +278,7 @@
<module>themes</module>
<module>model</module>
<module>util</module>
<module>rest</module>
<module>integration</module>
<module>adapters</module>
<module>authz</module>
@ -1450,6 +1451,11 @@
<artifactId>keycloak-admin-ui</artifactId>
<version>${keycloak.admin-ui.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-rest-admin-ui-ext</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Openshift -->
<dependency>

View file

@ -680,6 +680,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-rest-admin-ui-ext</artifactId>
</dependency>
</dependencies>
</profile>
</profiles>

72
rest/admin-ui-ext/pom.xml Normal file
View 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>

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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");
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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());
}
}

View file

@ -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();
}
}

View file

@ -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());
}
}

View file

@ -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 + '\'' + '}';
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View 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
View 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>

View file

@ -223,6 +223,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-rest-admin-ui-ext</artifactId>
</dependency>
</dependencies>
</dependencyManagement>

View file

@ -64,6 +64,10 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-rest-admin-ui-ext</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-admin-client</artifactId>