RealmAdminResource.java
/*
* Copyright 2016 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.
*/
package org.keycloak.services.resources.admin;
import static org.keycloak.utils.LockObjectsForModification.lockUserSessionsForModification;
import static org.keycloak.util.JsonSerialization.readValue;
import java.io.InputStream;
import java.security.cert.X509Certificate;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.eclipse.microprofile.openapi.annotations.Operation;
import org.eclipse.microprofile.openapi.annotations.extensions.Extension;
import org.eclipse.microprofile.openapi.annotations.parameters.Parameter;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.FormParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
import jakarta.ws.rs.core.StreamingOutput;
import com.fasterxml.jackson.core.type.TypeReference;
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
import org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.Config;
import org.keycloak.KeyPairVerifier;
import org.keycloak.authentication.CredentialRegistrator;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.Profile;
import org.keycloak.common.VerificationException;
import org.keycloak.common.util.PemUtils;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.EventQuery;
import org.keycloak.events.EventStoreProvider;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.AdminEventQuery;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.exportimport.ClientDescriptionConverter;
import org.keycloak.exportimport.ClientDescriptionConverterFactory;
import org.keycloak.exportimport.ExportAdapter;
import org.keycloak.exportimport.ExportOptions;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
import org.keycloak.models.Constants;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.models.utils.StripSecretsUtils;
import org.keycloak.partialimport.ErrorResponseException;
import org.keycloak.partialimport.PartialImportResult;
import org.keycloak.partialimport.PartialImportResults;
import org.keycloak.provider.InvalidationHandler;
import org.keycloak.representations.adapters.action.GlobalRequestResult;
import org.keycloak.representations.idm.AdminEventRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.representations.idm.ComponentRepresentation;
import org.keycloak.representations.idm.EventRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.ManagementPermissionReference;
import org.keycloak.representations.idm.RealmEventsConfigRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.services.managers.ResourceAdminManager;
import org.keycloak.services.resources.KeycloakOpenAPI;
import org.keycloak.services.resources.admin.ext.AdminRealmResourceProvider;
import org.keycloak.services.resources.admin.permissions.AdminPermissionEvaluator;
import org.keycloak.services.resources.admin.permissions.AdminPermissionManagement;
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
import org.keycloak.storage.DatastoreProvider;
import org.keycloak.storage.ExportImportManager;
import org.keycloak.storage.LegacyStoreSyncEvent;
import org.keycloak.utils.ProfileHelper;
import org.keycloak.utils.ReservedCharValidator;
/**
* Base resource class for the admin REST api of one realm
*
* @resource Realms Admin
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@Extension(name = KeycloakOpenAPI.Profiles.ADMIN, value = "")
public class RealmAdminResource {
protected static final Logger logger = Logger.getLogger(RealmAdminResource.class);
protected final AdminPermissionEvaluator auth;
protected final RealmModel realm;
private final AdminEventBuilder adminEvent;
protected final KeycloakSession session;
protected final ClientConnection connection;
protected final HttpHeaders headers;
public RealmAdminResource(KeycloakSession session, AdminPermissionEvaluator auth, AdminEventBuilder adminEvent) {
this.session = session;
this.auth = auth;
this.realm = session.getContext().getRealm();
this.connection = session.getContext().getConnection();
this.adminEvent = adminEvent.resource(ResourceType.REALM);
this.headers = session.getContext().getRequestHeaders();
}
/**
* Base path for importing clients under this realm.
*
* @return
*/
@Path("client-description-converter")
@Consumes({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML, MediaType.TEXT_PLAIN })
@POST
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Base path for importing clients under this realm.")
public ClientRepresentation convertClientDescription(String description) {
auth.clients().requireManage();
if (realm == null) {
throw new NotFoundException("Realm not found.");
}
return session.getKeycloakSessionFactory().getProviderFactoriesStream(ClientDescriptionConverter.class)
.map(ClientDescriptionConverterFactory.class::cast)
.filter(factory -> factory.isSupported(description))
.map(factory -> factory.create(session).convertToInternal(description))
.findFirst()
.orElseThrow(() -> new BadRequestException("Unsupported format"));
}
/**
* Base path for managing attack detection.
*
* @return
*/
@Path("attack-detection")
public AttackDetectionResource getAttackDetection() {
return new AttackDetectionResource(session, auth, adminEvent);
}
/**
* Base path for managing clients under this realm.
*
* @return
*/
@Path("clients")
public ClientsResource getClients() {
return new ClientsResource(session, auth, adminEvent);
}
/**
* This endpoint is deprecated. It's here just because of backwards compatibility. Use {@link #getClientScopes()} instead
*
* @return
*/
@Deprecated
@Path("client-templates")
public ClientScopesResource getClientTemplates() {
return getClientScopes();
}
/**
* Base path for managing client scopes under this realm.
*
* @return
*/
@Path("client-scopes")
public ClientScopesResource getClientScopes() {
return new ClientScopesResource(session, auth, adminEvent);
}
/**
* Base path for managing localization under this realm.
*/
@Path("localization")
public RealmLocalizationResource getLocalization() {
return new RealmLocalizationResource(session, auth);
}
/**
* Get realm default client scopes. Only name and ids are returned.
*
* @return
*/
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Path("default-default-client-scopes")
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Get realm default client scopes. Only name and ids are returned.")
public Stream<ClientScopeRepresentation> getDefaultDefaultClientScopes() {
return getDefaultClientScopes(true);
}
private Stream<ClientScopeRepresentation> getDefaultClientScopes(boolean defaultScope) {
auth.clients().requireViewClientScopes();
return realm.getDefaultClientScopesStream(defaultScope).map(clientScope -> {
ClientScopeRepresentation rep = new ClientScopeRepresentation();
rep.setId(clientScope.getId());
rep.setName(clientScope.getName());
rep.setProtocol(clientScope.getProtocol());
return rep;
});
}
@PUT
@NoCache
@Path("default-default-client-scopes/{clientScopeId}")
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation()
public void addDefaultDefaultClientScope(@PathParam("clientScopeId") String clientScopeId) {
addDefaultClientScope(clientScopeId,true);
}
private void addDefaultClientScope(String clientScopeId, boolean defaultScope) {
auth.clients().requireManageClientScopes();
ClientScopeModel clientScope = realm.getClientScopeById(clientScopeId);
if (clientScope == null) {
throw new NotFoundException("Client scope not found");
}
realm.addDefaultClientScope(clientScope, defaultScope);
adminEvent.operation(OperationType.CREATE).resource(ResourceType.CLIENT_SCOPE).resourcePath(session.getContext().getUri()).success();
}
@DELETE
@NoCache
@Path("default-default-client-scopes/{clientScopeId}")
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation()
public void removeDefaultDefaultClientScope(@PathParam("clientScopeId") String clientScopeId) {
auth.clients().requireManageClientScopes();
ClientScopeModel clientScope = realm.getClientScopeById(clientScopeId);
if (clientScope == null) {
throw new NotFoundException("Client scope not found");
}
realm.removeDefaultClientScope(clientScope);
adminEvent.operation(OperationType.DELETE).resource(ResourceType.CLIENT_SCOPE).resourcePath(session.getContext().getUri()).success();
}
/**
* Get realm optional client scopes. Only name and ids are returned.
*
* @return
*/
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Path("default-optional-client-scopes")
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Get realm optional client scopes. Only name and ids are returned.")
public Stream<ClientScopeRepresentation> getDefaultOptionalClientScopes() {
return getDefaultClientScopes(false);
}
@PUT
@NoCache
@Path("default-optional-client-scopes/{clientScopeId}")
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation()
public void addDefaultOptionalClientScope(@PathParam("clientScopeId") String clientScopeId) {
addDefaultClientScope(clientScopeId, false);
}
@DELETE
@NoCache
@Path("default-optional-client-scopes/{clientScopeId}")
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation()
public void removeDefaultOptionalClientScope(@PathParam("clientScopeId") String clientScopeId) {
removeDefaultDefaultClientScope(clientScopeId);
}
/**
* Base path for managing client initial access tokens
*
* @return
*/
@Path("clients-initial-access")
public ClientInitialAccessResource getClientInitialAccess() {
return new ClientInitialAccessResource(session, auth, adminEvent);
}
@Path("client-registration-policy")
public ClientRegistrationPolicyResource getClientRegistrationPolicy() {
return new ClientRegistrationPolicyResource(session, auth, adminEvent);
}
/**
* Base path for managing components under this realm.
*
* @return
*/
@Path("components")
public ComponentResource getComponents() {
return new ComponentResource(session, auth, adminEvent);
}
/**
* base path for managing realm-level roles of this realm
*
* @return
*/
@Path("roles")
public RoleContainerResource getRoleContainerResource() {
return new RoleContainerResource(session, session.getContext().getUri(), realm, auth, realm, adminEvent);
}
/**
* Get the top-level representation of the realm
*
* It will not include nested information like User and Client representations.
*
* @return
*/
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Get the top-level representation of the realm It will not include nested information like User and Client representations.")
public RealmRepresentation getRealm() {
if (auth.realm().canViewRealm()) {
return ModelToRepresentation.toRepresentation(session, realm, false);
} else {
auth.realm().requireViewRealmNameList();
RealmRepresentation rep = new RealmRepresentation();
rep.setRealm(realm.getName());
if (auth.users().canView()) {
rep.setRegistrationEmailAsUsername(realm.isRegistrationEmailAsUsername());
}
if (auth.realm().canViewIdentityProviders()) {
RealmRepresentation r = ModelToRepresentation.toRepresentation(session, realm, false);
rep.setIdentityProviders(r.getIdentityProviders());
rep.setIdentityProviderMappers(r.getIdentityProviderMappers());
}
return rep;
}
}
/**
* Update the top-level information of the realm
*
* Any user, roles or client information in the representation
* will be ignored. This will only update top-level attributes of the realm.
*
* @param rep
* @return
*/
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Update the top-level information of the realm Any user, roles or client information in the representation will be ignored.",
description = "This will only update top-level attributes of the realm.")
public Response updateRealm(final RealmRepresentation rep) {
auth.realm().requireManageRealm();
logger.debug("updating realm: " + realm.getName());
if (Config.getAdminRealm().equals(realm.getName()) && (rep.getRealm() != null && !rep.getRealm().equals(Config.getAdminRealm()))) {
throw ErrorResponse.error("Can't rename master realm", Status.BAD_REQUEST);
}
ReservedCharValidator.validate(rep.getRealm());
ReservedCharValidator.validateLocales(rep.getSupportedLocales());
try {
if (!Constants.GENERATE.equals(rep.getPublicKey()) && (rep.getPrivateKey() != null && rep.getPublicKey() != null)) {
try {
KeyPairVerifier.verify(rep.getPrivateKey(), rep.getPublicKey());
} catch (VerificationException e) {
throw ErrorResponse.error(e.getMessage(), Status.BAD_REQUEST);
}
}
if (!Constants.GENERATE.equals(rep.getPublicKey()) && (rep.getCertificate() != null)) {
try {
X509Certificate cert = PemUtils.decodeCertificate(rep.getCertificate());
if (cert == null) {
throw ErrorResponse.error("Failed to decode certificate", Status.BAD_REQUEST);
}
} catch (Exception e) {
throw ErrorResponse.error("Failed to decode certificate", Status.BAD_REQUEST);
}
}
boolean wasDuplicateEmailsAllowed = realm.isDuplicateEmailsAllowed();
RepresentationToModel.updateRealm(rep, realm, session);
// Refresh periodic sync tasks for configured federationProviders
LegacyStoreSyncEvent.fire(session, realm, false);
// This populates the map in DefaultKeycloakContext to be used when treating the event
session.getContext().getUri();
adminEvent.operation(OperationType.UPDATE).representation(StripSecretsUtils.strip(rep)).success();
if (rep.isDuplicateEmailsAllowed() != null && rep.isDuplicateEmailsAllowed() != wasDuplicateEmailsAllowed) {
session.invalidate(InvalidationHandler.ObjectType.REALM, realm.getId());
}
return Response.noContent().build();
} catch (ModelDuplicateException e) {
throw ErrorResponse.exists("Realm with same name exists");
} catch (ModelException e) {
throw ErrorResponse.error(e.getMessage(), Status.BAD_REQUEST);
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw ErrorResponse.error("Failed to update realm", Response.Status.INTERNAL_SERVER_ERROR);
}
}
/**
* Delete the realm
*
*/
@DELETE
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Delete the realm")
public void deleteRealm() {
auth.realm().requireManageRealm();
if (!new RealmManager(session).removeRealm(realm)) {
throw new NotFoundException("Realm doesn't exist");
}
// The delete event is associated with the realm of the user executing the operation,
// instead of the realm being deleted.
AdminEventBuilder deleteAdminEvent = new AdminEventBuilder(auth.adminAuth().getRealm(), auth.adminAuth(), session, connection);
deleteAdminEvent.operation(OperationType.DELETE).resource(ResourceType.REALM)
.realm(auth.adminAuth().getRealm().getId()).resourcePath(realm.getName()).success();
}
/**
* Base path for managing users in this realm.
*
* @return
*/
@Path("users")
public UsersResource users() {
return new UsersResource(session, auth, adminEvent);
}
@NoCache
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("users-management-permissions")
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation()
public ManagementPermissionReference getUserMgmtPermissions() {
auth.realm().requireViewRealm();
AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
if (permissions.users().isPermissionsEnabled()) {
return toUsersMgmtRef(permissions);
} else {
return new ManagementPermissionReference();
}
}
@PUT
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@NoCache
@Path("users-management-permissions")
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation()
public ManagementPermissionReference setUsersManagementPermissionsEnabled(ManagementPermissionReference ref) {
auth.realm().requireManageRealm();
AdminPermissionManagement permissions = AdminPermissions.management(session, realm);
permissions.users().setPermissionsEnabled(ref.isEnabled());
if (ref.isEnabled()) {
return toUsersMgmtRef(permissions);
} else {
return new ManagementPermissionReference();
}
}
public static ManagementPermissionReference toUsersMgmtRef(AdminPermissionManagement permissions) {
ManagementPermissionReference ref = new ManagementPermissionReference();
ref.setEnabled(true);
ref.setResource(permissions.users().resource().getId());
Map<String, String> scopes = permissions.users().getPermissions();
ref.setScopePermissions(scopes);
return ref;
}
@Path("{extension}")
public Object extension(@PathParam("extension") String extension) {
AdminRealmResourceProvider provider = session.getProvider(AdminRealmResourceProvider.class, extension);
if (provider != null) {
Object resource = provider.getResource(session, realm, auth, adminEvent);
if (resource != null) {
return resource;
}
}
throw new NotFoundException();
}
@Path("authentication")
public AuthenticationManagementResource flows() {
return new AuthenticationManagementResource(session, auth, adminEvent);
}
/**
* Path for managing all realm-level or client-level roles defined in this realm by its id.
*
* @return
*/
@Path("roles-by-id")
public RoleByIdResource rolesById() {
return new RoleByIdResource(session, auth, adminEvent);
}
/**
* Push the realm's revocation policy to any client that has an admin url associated with it.
*
*/
@Path("push-revocation")
@Produces(MediaType.APPLICATION_JSON)
@POST
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Push the realm's revocation policy to any client that has an admin url associated with it.")
public GlobalRequestResult pushRevocation() {
auth.realm().requireManageRealm();
GlobalRequestResult result = new ResourceAdminManager(session).pushRealmRevocationPolicy(realm);
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(result).success();
return result;
}
/**
* Removes all user sessions. Any client that has an admin url will also be told to invalidate any sessions
* they have.
*
*/
@Path("logout-all")
@POST
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Removes all user sessions.", description = "Any client that has an admin url will also be told to invalidate any sessions they have.")
public GlobalRequestResult logoutAll() {
auth.users().requireManage();
session.sessions().removeUserSessions(realm);
GlobalRequestResult result = new ResourceAdminManager(session).logoutAll(realm);
adminEvent.operation(OperationType.ACTION).resourcePath(session.getContext().getUri()).representation(result).success();
return result;
}
/**
* Remove a specific user session. Any client that has an admin url will also be told to invalidate this
* particular session.
*
* @param sessionId
*/
@Path("sessions/{session}")
@DELETE
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Remove a specific user session.", description = "Any client that has an admin url will also be told to invalidate this particular session.")
public void deleteSession(@PathParam("session") String sessionId) {
auth.users().requireManage();
UserSessionModel userSession = lockUserSessionsForModification(session, () -> session.sessions().getUserSession(realm, sessionId));
if (userSession == null) throw new NotFoundException("Sesssion not found");
AuthenticationManager.backchannelLogout(session, realm, userSession, session.getContext().getUri(), connection, headers, true);
adminEvent.operation(OperationType.DELETE).resource(ResourceType.USER_SESSION).resourcePath(session.getContext().getUri()).success();
}
/**
* Get client session stats
*
* Returns a JSON map. The key is the client id, the value is the number of sessions that currently are active
* with that client. Only clients that actually have a session associated with them will be in this map.
*
* @return
*/
@Path("client-session-stats")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Get client session stats Returns a JSON map.",
description = "The key is the client id, the value is the number of sessions that currently are active with that client. Only clients that actually have a session associated with them will be in this map.")
public Stream<Map<String, String>> getClientSessionStats() {
auth.realm().requireViewRealm();
Map<String, Map<String, String>> data = new HashMap<>();
{
Map<String, Long> activeCount = session.sessions().getActiveClientSessionStats(realm, false);
for (Map.Entry<String, Long> entry : activeCount.entrySet()) {
Map<String, String> map = new HashMap<>();
ClientModel client = realm.getClientById(entry.getKey());
if (client == null)
continue;
map.put("id", client.getId());
map.put("clientId", client.getClientId());
map.put("active", entry.getValue().toString());
map.put("offline", "0");
data.put(client.getId(), map);
}
}
{
Map<String, Long> offlineCount = session.sessions().getActiveClientSessionStats(realm, true);
for (Map.Entry<String, Long> entry : offlineCount.entrySet()) {
Map<String, String> map = data.get(entry.getKey());
if (map == null) {
map = new HashMap<>();
ClientModel client = realm.getClientById(entry.getKey());
if (client == null)
continue;
map.put("id", client.getId());
map.put("clientId", client.getClientId());
map.put("active", "0");
data.put(client.getId(), map);
}
map.put("offline", entry.getValue().toString());
}
}
return data.values().stream();
}
/**
* Get the events provider configuration
*
* Returns JSON object with events provider configuration
*
* @return
*/
@GET
@NoCache
@Path("events/config")
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Get the events provider configuration Returns JSON object with events provider configuration")
public RealmEventsConfigRepresentation getRealmEventsConfig() {
auth.realm().requireViewEvents();
RealmEventsConfigRepresentation config = ModelToRepresentation.toEventsConfigReprensetation(realm);
if (config.getEnabledEventTypes() == null || config.getEnabledEventTypes().isEmpty()) {
List<String> eventTypes = Arrays.stream(EventType.values())
.filter(EventType::isSaveByDefault)
.map(EventType::name)
.collect(Collectors.toList());
config.setEnabledEventTypes(eventTypes);
}
return config;
}
/**
* Update the events provider
*
* Change the events provider and/or its configuration
*
* @param rep
*/
@PUT
@Path("events/config")
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( description = "Update the events provider Change the events provider and/or its configuration")
public void updateRealmEventsConfig(final RealmEventsConfigRepresentation rep) {
auth.realm().requireManageEvents();
logger.debug("updating realm events config: " + realm.getName());
new RealmManager(session).updateRealmEventsConfig(rep, realm);
adminEvent.operation(OperationType.UPDATE).resource(ResourceType.REALM)
.resourcePath(session.getContext().getUri()).representation(rep)
// refresh the builder to consider old and new config
.refreshRealmEventsConfig(session)
.success();
}
/**
* Get events
*
* Returns all events, or filters them based on URL query parameters listed here
*
* @param types The types of events to return
* @param client App or oauth client name
* @param user User id
* @param ipAddress IP address
* @param dateTo To date
* @param dateFrom From date
* @param firstResult Paging offset
* @param maxResults Maximum results size (defaults to 100)
* @return
*/
@Path("events")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Get events Returns all events, or filters them based on URL query parameters listed here")
public Stream<EventRepresentation> getEvents(@Parameter(description = "The types of events to return") @QueryParam("type") List<String> types,
@Parameter(description = "App or oauth client name") @QueryParam("client") String client,
@Parameter(description = "User id") @QueryParam("user") String user,
@Parameter(description = "From date") @QueryParam("dateFrom") String dateFrom,
@Parameter(description = "To date") @QueryParam("dateTo") String dateTo,
@Parameter(description = "IP Address") @QueryParam("ipAddress") String ipAddress,
@Parameter(description = "Paging offset") @QueryParam("first") Integer firstResult,
@Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults) {
auth.realm().requireViewEvents();
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
EventQuery query = eventStore.createQuery().realm(realm.getId());
if (client != null) {
query.client(client);
}
if (types != null && !types.isEmpty()) {
EventType[] t = new EventType[types.size()];
for (int i = 0; i < t.length; i++) {
t[i] = EventType.valueOf(types.get(i));
}
query.type(t);
}
if (user != null) {
query.user(user);
}
if(dateFrom != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date from = null;
try {
from = df.parse(dateFrom);
} catch (ParseException e) {
throw new BadRequestException("Invalid value for 'Date(From)', expected format is yyyy-MM-dd");
}
query.fromDate(from);
}
if(dateTo != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date to = null;
try {
to = df.parse(dateTo);
} catch (ParseException e) {
throw new BadRequestException("Invalid value for 'Date(To)', expected format is yyyy-MM-dd");
}
query.toDate(to);
}
if (ipAddress != null) {
query.ipAddress(ipAddress);
}
if (firstResult != null) {
query.firstResult(firstResult);
}
if (maxResults != null) {
query.maxResults(maxResults);
} else {
query.maxResults(Constants.DEFAULT_MAX_RESULTS);
}
return query.getResultStream().map(ModelToRepresentation::toRepresentation);
}
/**
* Get admin events
*
* Returns all admin events, or filters events based on URL query parameters listed here
*
* @param operationTypes
* @param authRealm
* @param authClient
* @param authUser user id
* @param authIpAddress
* @param resourcePath
* @param dateTo
* @param dateFrom
* @param firstResult
* @param maxResults Maximum results size (defaults to 100)
* @return
*/
@Path("admin-events")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Get admin events Returns all admin events, or filters events based on URL query parameters listed here")
public Stream<AdminEventRepresentation> getEvents(@QueryParam("operationTypes") List<String> operationTypes, @QueryParam("authRealm") String authRealm, @QueryParam("authClient") String authClient,
@Parameter(description = "user id") @QueryParam("authUser") String authUser, @QueryParam("authIpAddress") String authIpAddress,
@QueryParam("resourcePath") String resourcePath, @QueryParam("dateFrom") String dateFrom,
@QueryParam("dateTo") String dateTo, @QueryParam("first") Integer firstResult,
@Parameter(description = "Maximum results size (defaults to 100)") @QueryParam("max") Integer maxResults,
@QueryParam("resourceTypes") List<String> resourceTypes) {
auth.realm().requireViewEvents();
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
AdminEventQuery query = eventStore.createAdminQuery().realm(realm.getId());;
if (authRealm != null) {
query.authRealm(authRealm);
}
if (authClient != null) {
query.authClient(authClient);
}
if (authUser != null) {
query.authUser(authUser);
}
if (authIpAddress != null) {
query.authIpAddress(authIpAddress);
}
if (resourcePath != null) {
query.resourcePath(resourcePath);
}
if (operationTypes != null && !operationTypes.isEmpty()) {
OperationType[] t = new OperationType[operationTypes.size()];
for (int i = 0; i < t.length; i++) {
t[i] = OperationType.valueOf(operationTypes.get(i));
}
query.operation(t);
}
if (resourceTypes != null && !resourceTypes.isEmpty()) {
ResourceType[] t = new ResourceType[resourceTypes.size()];
for (int i = 0; i < t.length; i++) {
t[i] = ResourceType.valueOf(resourceTypes.get(i));
}
query.resourceType(t);
}
if(dateFrom != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date from = null;
try {
from = df.parse(dateFrom);
} catch (ParseException e) {
throw new BadRequestException("Invalid value for 'Date(From)', expected format is yyyy-MM-dd");
}
query.fromTime(from);
}
if(dateTo != null) {
SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd");
Date to = null;
try {
to = df.parse(dateTo);
} catch (ParseException e) {
throw new BadRequestException("Invalid value for 'Date(To)', expected format is yyyy-MM-dd");
}
query.toTime(to);
}
if (firstResult != null) {
query.firstResult(firstResult);
}
if (maxResults != null) {
query.maxResults(maxResults);
} else {
query.maxResults(Constants.DEFAULT_MAX_RESULTS);
}
return query.getResultStream().map(ModelToRepresentation::toRepresentation);
}
/**
* Delete all events
*
*/
@Path("events")
@DELETE
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Delete all events")
public void clearEvents() {
auth.realm().requireManageEvents();
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
eventStore.clear(realm);
}
/**
* Delete all admin events
*
*/
@Path("admin-events")
@DELETE
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Delete all admin events")
public void clearAdminEvents() {
auth.realm().requireManageEvents();
EventStoreProvider eventStore = session.getProvider(EventStoreProvider.class);
eventStore.clearAdmin(realm);
}
/**
* Test SMTP connection with current logged in user
*
* @param config SMTP server configuration
* @return
* @throws Exception
*/
@Path("testSMTPConnection")
@POST
@NoCache
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@Deprecated
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Test SMTP connection with current logged in user")
public Response testSMTPConnection(final @Parameter(description = "SMTP server configuration") @FormParam("config") String config) throws Exception {
Map<String, String> settings = readValue(config, new TypeReference<Map<String, String>>() {
});
return testSMTPConnection(settings);
}
@Path("testSMTPConnection")
@POST
@NoCache
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation()
public Response testSMTPConnection(Map<String, String> settings) throws Exception {
try {
UserModel user = auth.adminAuth().getUser();
if (user.getEmail() == null) {
throw ErrorResponse.error("Logged in user does not have an e-mail.", Response.Status.INTERNAL_SERVER_ERROR);
}
if (ComponentRepresentation.SECRET_VALUE.equals(settings.get("password"))) {
settings.put("password", realm.getSmtpConfig().get("password"));
}
session.getProvider(EmailTemplateProvider.class).sendSmtpTestEmail(settings, user);
} catch (Exception e) {
e.printStackTrace();
logger.errorf("Failed to send email \n %s", e.getCause());
throw ErrorResponse.error("Failed to send email", Response.Status.INTERNAL_SERVER_ERROR);
}
return Response.noContent().build();
}
@Path("identity-provider")
public IdentityProvidersResource getIdentityProviderResource() {
return new IdentityProvidersResource(realm, session, this.auth, adminEvent);
}
/**
* Get group hierarchy. Only name and ids are returned.
*
* @return
*/
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Path("default-groups")
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Get group hierarchy. Only name and ids are returned.")
public Stream<GroupRepresentation> getDefaultGroups() {
auth.realm().requireViewRealm();
return realm.getDefaultGroupsStream().map(ModelToRepresentation::groupToBriefRepresentation);
}
@PUT
@NoCache
@Path("default-groups/{groupId}")
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation()
public void addDefaultGroup(@PathParam("groupId") String groupId) {
auth.realm().requireManageRealm();
GroupModel group = realm.getGroupById(groupId);
if (group == null) {
throw new NotFoundException("Group not found");
}
realm.addDefaultGroup(group);
adminEvent.operation(OperationType.CREATE).resource(ResourceType.GROUP).resourcePath(session.getContext().getUri()).success();
}
@DELETE
@NoCache
@Path("default-groups/{groupId}")
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation()
public void removeDefaultGroup(@PathParam("groupId") String groupId) {
auth.realm().requireManageRealm();
GroupModel group = realm.getGroupById(groupId);
if (group == null) {
throw new NotFoundException("Group not found");
}
realm.removeDefaultGroup(group);
adminEvent.operation(OperationType.DELETE).resource(ResourceType.GROUP).resourcePath(session.getContext().getUri()).success();
}
@Path("groups")
public GroupsResource getGroups() {
return new GroupsResource(realm, session, this.auth, adminEvent);
}
@GET
@Path("group-by-path/{path: .*}")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation()
public GroupRepresentation getGroupByPath(@PathParam("path") String path) {
GroupModel found = KeycloakModelUtils.findGroupByPath(realm, path);
if (found == null) {
throw new NotFoundException("Group path does not exist");
}
auth.groups().requireView(found);
return ModelToRepresentation.toGroupHierarchy(found, true);
}
/**
* Partial import from a JSON file to an existing realm.
*
*/
@Path("partialImport")
@POST
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Partial import from a JSON file to an existing realm.")
public Response partialImport(InputStream requestBody) {
auth.realm().requireManageRealm();
try {
return Response.ok(
KeycloakModelUtils.runJobInTransactionWithResult(session.getKeycloakSessionFactory(), kcSession -> {
RealmModel realmClone = kcSession.realms().getRealm(realm.getId());
AdminEventBuilder adminEventClone = adminEvent.clone(kcSession);
// calling a static method to avoid using the wrong instances
return getPartialImportResults(requestBody, kcSession, realmClone, adminEventClone);
})
).build();
} catch (ModelDuplicateException e) {
throw ErrorResponse.exists(e.getLocalizedMessage());
} catch (ErrorResponseException error) {
return error.getResponse();
} catch (Exception e) {
throw ErrorResponse.error(e.getMessage(), Response.Status.INTERNAL_SERVER_ERROR);
}
}
private static PartialImportResults getPartialImportResults(InputStream requestBody, KeycloakSession kcSession, RealmModel kcRealm, AdminEventBuilder adminEventClone) {
ExportImportManager exportProvider = kcSession.getProvider(DatastoreProvider.class).getExportImportManager();
PartialImportResults results = exportProvider.partialImportRealm(kcRealm, requestBody);
for (PartialImportResult result : results.getResults()) {
switch (result.getAction()) {
case ADDED : fireCreatedEvent(result, adminEventClone); break;
case OVERWRITTEN: fireUpdateEvent(result, adminEventClone); break;
}
}
return results;
}
private static void fireCreatedEvent(PartialImportResult result, AdminEventBuilder adminEvent) {
adminEvent.operation(OperationType.CREATE)
.resourcePath(result.getResourceType().getPath(), result.getId())
.representation(result.getRepresentation())
.success();
};
private static void fireUpdateEvent(PartialImportResult result, AdminEventBuilder adminEvent) {
adminEvent.operation(OperationType.UPDATE)
.resourcePath(result.getResourceType().getPath(), result.getId())
.representation(result.getRepresentation())
.success();
}
/**
* Partial export of existing realm into a JSON file.
*
* @param exportGroupsAndRoles
* @param exportClients
* @return
*/
@Path("partial-export")
@Produces(MediaType.APPLICATION_JSON)
@POST
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation( summary = "Partial export of existing realm into a JSON file.")
public Response partialExport(@QueryParam("exportGroupsAndRoles") Boolean exportGroupsAndRoles,
@QueryParam("exportClients") Boolean exportClients) {
auth.realm().requireViewRealm();
boolean groupsAndRolesExported = exportGroupsAndRoles != null && exportGroupsAndRoles;
boolean clientsExported = exportClients != null && exportClients;
if (groupsAndRolesExported) {
auth.groups().requireList();
}
if (clientsExported) {
auth.clients().requireView();
}
// service accounts are exported if the clients are exported
// this means that if clients is true but groups/roles is false the service account is exported without roles
// the other option is just include service accounts if clientsExported && groupsAndRolesExported
ExportOptions options = new ExportOptions(false, clientsExported, groupsAndRolesExported, clientsExported);
ExportImportManager exportProvider = session.getProvider(DatastoreProvider.class).getExportImportManager();
Response.ResponseBuilder response = Response.ok();
exportProvider.exportRealm(realm, options, new ExportAdapter() {
@Override
public void setType(String mediaType) {
response.type(mediaType);
}
@Override
public void writeToOutputStream(ConsumerOfOutputStream consumer) {
response.entity((StreamingOutput) consumer::accept);
}
});
return response.build();
}
@Path("keys")
public KeyResource keys() {
return new KeyResource(realm, session, this.auth);
}
@GET
@Path("credential-registrators")
@NoCache
@Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON)
@Tag(name = KeycloakOpenAPI.Admin.Tags.REALMS_ADMIN)
@Operation()
public Stream<String> getCredentialRegistrators(){
auth.realm().requireViewRealm();
return session.getContext().getRealm().getRequiredActionProvidersStream()
.filter(RequiredActionProviderModel::isEnabled)
.map(RequiredActionProviderModel::getProviderId)
.filter(providerId -> session.getProvider(RequiredActionProvider.class, providerId) instanceof CredentialRegistrator);
}
@Path("client-policies/policies")
public ClientPoliciesResource getClientPoliciesResource() {
ProfileHelper.requireFeature(Profile.Feature.CLIENT_POLICIES);
return new ClientPoliciesResource(session, auth);
}
@Path("client-policies/profiles")
public ClientProfilesResource getClientProfilesResource() {
ProfileHelper.requireFeature(Profile.Feature.CLIENT_POLICIES);
return new ClientProfilesResource(session, auth);
}
}