/*
* 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 org.jboss.logging.Logger;
import org.jboss.resteasy.annotations.cache.NoCache;
import org.jboss.resteasy.spi.BadRequestException;
import org.jboss.resteasy.spi.NotFoundException;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.authentication.RequiredActionProvider;
import org.keycloak.authentication.actiontoken.execactions.ExecuteActionsActionToken;
import org.keycloak.common.ClientConnection;
import org.keycloak.common.Profile;
import org.keycloak.common.util.Time;
import org.keycloak.credential.CredentialModel;
import org.keycloak.email.EmailException;
import org.keycloak.email.EmailTemplateProvider;
import org.keycloak.events.Details;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType;
import org.keycloak.models.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.utils.RedirectUtils;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.UserConsentRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.services.ErrorResponse;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.*;
import org.keycloak.services.managers.*;
import org.keycloak.services.resources.AccountService;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.validation.Validation;
import org.keycloak.storage.ReadOnlyException;
import org.keycloak.utils.ProfileHelper;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.text.MessageFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
/**
* Base resource for managing users
*
* @resource Users
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class UsersResource {
private static final Logger logger = Logger.getLogger(UsersResource.class);
protected RealmModel realm;
private RealmAuth auth;
private AdminEventBuilder adminEvent;
@Context
protected ClientConnection clientConnection;
@Context
protected UriInfo uriInfo;
@Context
protected KeycloakSession session;
@Context
protected HttpHeaders headers;
public UsersResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) {
this.auth = auth;
this.realm = realm;
this.adminEvent = adminEvent.resource(ResourceType.USER);
auth.init(RealmAuth.Resource.USER);
}
/**
* Update the user
*
* @param id User id
* @param rep
* @return
*/
@Path("{id}")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public Response updateUser(final @PathParam("id") String id, final UserRepresentation rep) {
auth.requireManage();
try {
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
return Response.status(Status.NOT_FOUND).build();
}
Set<String> attrsToRemove;
if (rep.getAttributes() != null) {
attrsToRemove = new HashSet<>(user.getAttributes().keySet());
attrsToRemove.removeAll(rep.getAttributes().keySet());
} else {
attrsToRemove = Collections.emptySet();
}
if (rep.isEnabled() != null && rep.isEnabled()) {
UserLoginFailureModel failureModel = session.sessions().getUserLoginFailure(realm, id);
if (failureModel != null) {
failureModel.clearFailures();
}
}
updateUserFromRep(user, rep, attrsToRemove, realm, session, true);
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(rep).success();
if (session.getTransactionManager().isActive()) {
session.getTransactionManager().commit();
}
return Response.noContent().build();
} catch (ModelDuplicateException e) {
return ErrorResponse.exists("User exists with same username or email");
} catch (ReadOnlyException re) {
return ErrorResponse.exists("User is read only!");
} catch (ModelException me) {
logger.warn("Could not update user!", me);
return ErrorResponse.exists("Could not update user!");
} catch (Exception me) { // JPA
logger.warn("Could not update user!", me);// may be committed by JTA which can't
return ErrorResponse.exists("Could not update user!");
}
}
/**
* Create a new user
*
* Username must be unique.
*
* @param uriInfo
* @param rep
* @return
*/
@POST
@Consumes(MediaType.APPLICATION_JSON)
public Response createUser(final @Context UriInfo uriInfo, final UserRepresentation rep) {
auth.requireManage();
// Double-check duplicated username and email here due to federation
if (session.users().getUserByUsername(rep.getUsername(), realm) != null) {
return ErrorResponse.exists("User exists with same username");
}
if (rep.getEmail() != null && !realm.isDuplicateEmailsAllowed() && session.users().getUserByEmail(rep.getEmail(), realm) != null) {
return ErrorResponse.exists("User exists with same email");
}
try {
UserModel user = session.users().addUser(realm, rep.getUsername());
Set<String> emptySet = Collections.emptySet();
updateUserFromRep(user, rep, emptySet, realm, session, false);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, user.getId()).representation(rep).success();
if (session.getTransactionManager().isActive()) {
session.getTransactionManager().commit();
}
return Response.created(uriInfo.getAbsolutePathBuilder().path(user.getId()).build()).build();
} catch (ModelDuplicateException e) {
if (session.getTransactionManager().isActive()) {
session.getTransactionManager().setRollbackOnly();
}
return ErrorResponse.exists("User exists with same username or email");
} catch (ModelException me){
if (session.getTransactionManager().isActive()) {
session.getTransactionManager().setRollbackOnly();
}
logger.warn("Could not create user", me);
return ErrorResponse.exists("Could not create user");
}
}
public static void updateUserFromRep(UserModel user, UserRepresentation rep, Set<String> attrsToRemove, RealmModel realm, KeycloakSession session, boolean removeMissingRequiredActions) {
if (rep.getUsername() != null && realm.isEditUsernameAllowed()) {
user.setUsername(rep.getUsername());
}
if (rep.getEmail() != null) user.setEmail(rep.getEmail());
if (rep.getFirstName() != null) user.setFirstName(rep.getFirstName());
if (rep.getLastName() != null) user.setLastName(rep.getLastName());
if (rep.isEnabled() != null) user.setEnabled(rep.isEnabled());
if (rep.isEmailVerified() != null) user.setEmailVerified(rep.isEmailVerified());
List<String> reqActions = rep.getRequiredActions();
if (reqActions != null) {
Set<String> allActions = new HashSet<>();
for (ProviderFactory factory : session.getKeycloakSessionFactory().getProviderFactories(RequiredActionProvider.class)) {
allActions.add(factory.getId());
}
for (String action : allActions) {
if (reqActions.contains(action)) {
user.addRequiredAction(action);
} else if (removeMissingRequiredActions) {
user.removeRequiredAction(action);
}
}
}
if (rep.getAttributes() != null) {
for (Map.Entry<String, List<String>> attr : rep.getAttributes().entrySet()) {
user.setAttribute(attr.getKey(), attr.getValue());
}
for (String attr : attrsToRemove) {
user.removeAttribute(attr);
}
}
}
/**
* Get representation of the user
*
* @param id User id
* @return
*/
@Path("{id}")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public UserRepresentation getUser(final @PathParam("id") String id) {
auth.requireView();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
UserRepresentation rep = ModelToRepresentation.toRepresentation(session, realm, user);
if (realm.isIdentityFederationEnabled()) {
List<FederatedIdentityRepresentation> reps = getFederatedIdentities(user);
rep.setFederatedIdentities(reps);
}
if (session.getProvider(BruteForceProtector.class).isTemporarilyDisabled(session, realm, user)) {
rep.setEnabled(false);
}
return rep;
}
/**
* Impersonate the user
*
* @param id User id
* @return
*/
@Path("{id}/impersonation")
@POST
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Object> impersonate(final @PathParam("id") String id) {
ProfileHelper.requireFeature(Profile.Feature.IMPERSONATION);
auth.init(RealmAuth.Resource.IMPERSONATION);
auth.requireManage();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
RealmModel authenticatedRealm = auth.getAuth().getRealm();
// if same realm logout before impersonation
boolean sameRealm = false;
if (authenticatedRealm.getId().equals(realm.getId())) {
sameRealm = true;
UserSessionModel userSession = session.sessions().getUserSession(authenticatedRealm, auth.getAuth().getToken().getSessionState());
AuthenticationManager.expireIdentityCookie(realm, uriInfo, clientConnection);
AuthenticationManager.expireRememberMeCookie(realm, uriInfo, clientConnection);
AuthenticationManager.backchannelLogout(session, authenticatedRealm, userSession, uriInfo, clientConnection, headers, true);
}
EventBuilder event = new EventBuilder(realm, session, clientConnection);
String sessionId = KeycloakModelUtils.generateId();
UserSessionModel userSession = session.sessions().createUserSession(sessionId, realm, user, user.getUsername(), clientConnection.getRemoteAddr(), "impersonate", false, null, null);
AuthenticationManager.createLoginCookie(session, realm, userSession.getUser(), userSession, uriInfo, clientConnection);
URI redirect = AccountService.accountServiceApplicationPage(uriInfo).build(realm.getName());
Map<String, Object> result = new HashMap<>();
result.put("sameRealm", sameRealm);
result.put("redirect", redirect.toString());
event.event(EventType.IMPERSONATE)
.session(userSession)
.user(user)
.detail(Details.IMPERSONATOR_REALM,authenticatedRealm.getName())
.detail(Details.IMPERSONATOR, auth.getAuth().getUser().getUsername()).success();
return result;
}
/**
* Get sessions associated with the user
*
* @param id User id
* @return
*/
@Path("{id}/sessions")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<UserSessionRepresentation> getSessions(final @PathParam("id") String id) {
auth.requireView();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
List<UserSessionModel> sessions = session.sessions().getUserSessions(realm, user);
List<UserSessionRepresentation> reps = new ArrayList<UserSessionRepresentation>();
for (UserSessionModel session : sessions) {
UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session);
reps.add(rep);
}
return reps;
}
/**
* Get offline sessions associated with the user and client
*
* @param id User id
* @return
*/
@Path("{id}/offline-sessions/{clientId}")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<UserSessionRepresentation> getOfflineSessions(final @PathParam("id") String id, final @PathParam("clientId") String clientId) {
auth.requireView();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
ClientModel client = realm.getClientById(clientId);
if (client == null) {
throw new NotFoundException("Client not found");
}
List<UserSessionModel> sessions = new UserSessionManager(session).findOfflineSessions(realm, user);
List<UserSessionRepresentation> reps = new ArrayList<UserSessionRepresentation>();
for (UserSessionModel session : sessions) {
UserSessionRepresentation rep = ModelToRepresentation.toRepresentation(session);
// Update lastSessionRefresh with the timestamp from clientSession
AuthenticatedClientSessionModel clientSession = session.getAuthenticatedClientSessions().get(clientId);
// Skip if userSession is not for this client
if (clientSession == null) {
continue;
}
rep.setLastAccess(clientSession.getTimestamp());
reps.add(rep);
}
return reps;
}
/**
* Get social logins associated with the user
*
* @param id User id
* @return
*/
@Path("{id}/federated-identity")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<FederatedIdentityRepresentation> getFederatedIdentity(final @PathParam("id") String id) {
auth.requireView();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
return getFederatedIdentities(user);
}
private List<FederatedIdentityRepresentation> getFederatedIdentities(UserModel user) {
Set<FederatedIdentityModel> identities = session.users().getFederatedIdentities(user, realm);
List<FederatedIdentityRepresentation> result = new ArrayList<FederatedIdentityRepresentation>();
for (FederatedIdentityModel identity : identities) {
for (IdentityProviderModel identityProviderModel : realm.getIdentityProviders()) {
if (identityProviderModel.getAlias().equals(identity.getIdentityProvider())) {
FederatedIdentityRepresentation rep = ModelToRepresentation.toRepresentation(identity);
result.add(rep);
}
}
}
return result;
}
/**
* Add a social login provider to the user
*
* @param id User id
* @param provider Social login provider id
* @param rep
* @return
*/
@Path("{id}/federated-identity/{provider}")
@POST
@NoCache
public Response addFederatedIdentity(final @PathParam("id") String id, final @PathParam("provider") String provider, FederatedIdentityRepresentation rep) {
auth.requireManage();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
if (session.users().getFederatedIdentity(user, provider, realm) != null) {
return ErrorResponse.exists("User is already linked with provider");
}
FederatedIdentityModel socialLink = new FederatedIdentityModel(provider, rep.getUserId(), rep.getUserName());
session.users().addFederatedIdentity(realm, user, socialLink);
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo).representation(rep).success();
return Response.noContent().build();
}
/**
* Remove a social login provider from user
*
* @param id User id
* @param provider Social login provider id
*/
@Path("{id}/federated-identity/{provider}")
@DELETE
@NoCache
public void removeFederatedIdentity(final @PathParam("id") String id, final @PathParam("provider") String provider) {
auth.requireManage();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
if (!session.users().removeFederatedIdentity(realm, user, provider)) {
throw new NotFoundException("Link not found");
}
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
}
/**
* Get consents granted by the user
*
* @param id User id
* @return
*/
@Path("{id}/consents")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<Map<String, Object>> getConsents(final @PathParam("id") String id) {
auth.requireView();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
List<Map<String, Object>> result = new LinkedList<>();
Set<ClientModel> offlineClients = new UserSessionManager(session).findClientsWithOfflineToken(realm, user);
for (ClientModel client : realm.getClients()) {
UserConsentModel consent = session.users().getConsentByClient(realm, user.getId(), client.getId());
boolean hasOfflineToken = offlineClients.contains(client);
if (consent == null && !hasOfflineToken) {
continue;
}
UserConsentRepresentation rep = (consent == null) ? null : ModelToRepresentation.toRepresentation(consent);
Map<String, Object> currentRep = new HashMap<>();
currentRep.put("clientId", client.getClientId());
currentRep.put("grantedProtocolMappers", (rep==null ? Collections.emptyMap() : rep.getGrantedProtocolMappers()));
currentRep.put("grantedRealmRoles", (rep==null ? Collections.emptyList() : rep.getGrantedRealmRoles()));
currentRep.put("grantedClientRoles", (rep==null ? Collections.emptyMap() : rep.getGrantedClientRoles()));
currentRep.put("createdDate", (rep==null ? null : rep.getCreatedDate()));
currentRep.put("lastUpdatedDate", (rep==null ? null : rep.getLastUpdatedDate()));
List<Map<String, String>> additionalGrants = new LinkedList<>();
if (hasOfflineToken) {
Map<String, String> offlineTokens = new HashMap<>();
offlineTokens.put("client", client.getId());
// TODO: translate
offlineTokens.put("key", "Offline Token");
additionalGrants.add(offlineTokens);
}
currentRep.put("additionalGrants", additionalGrants);
result.add(currentRep);
}
return result;
}
/**
* Revoke consent and offline tokens for particular client from user
*
* @param id User id
* @param clientId Client id
*/
@Path("{id}/consents/{client}")
@DELETE
@NoCache
public void revokeConsent(final @PathParam("id") String id, final @PathParam("client") String clientId) {
auth.requireManage();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
ClientModel client = realm.getClientByClientId(clientId);
boolean revokedConsent = session.users().revokeConsentForClient(realm, user.getId(), client.getId());
boolean revokedOfflineToken = new UserSessionManager(session).revokeOfflineToken(user, client);
if (revokedConsent) {
// Logout clientSessions for this user and client
AuthenticationManager.backchannelUserFromClient(session, realm, user, client, uriInfo, headers);
}
if (!revokedConsent && !revokedOfflineToken) {
throw new NotFoundException("Consent nor offline token not found");
}
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
}
/**
* Remove all user sessions associated with the user
*
* Also send notification to all clients that have an admin URL to invalidate the sessions for the particular user.
*
* @param id User id
*/
@Path("{id}/logout")
@POST
public void logout(final @PathParam("id") String id) {
auth.requireManage();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
for (UserSessionModel userSession : userSessions) {
AuthenticationManager.backchannelLogout(session, realm, userSession, uriInfo, clientConnection, headers, true);
}
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
}
/**
* Delete the user
*
* @param id User id
*/
@Path("{id}")
@DELETE
@NoCache
public Response deleteUser(final @PathParam("id") String id) {
auth.requireManage();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
boolean removed = new UserManager(session).removeUser(realm, user);
if (removed) {
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
return Response.noContent().build();
} else {
return ErrorResponse.error("User couldn't be deleted", Response.Status.BAD_REQUEST);
}
}
/**
* Get users
*
* Returns a list of users, filtered according to query parameters
*
* @param search A String contained in username, first or last name, or email
* @param last
* @param first
* @param email
* @param username
* @param first Pagination offset
* @param maxResults Maximum results size (defaults to 100)
* @return
*/
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<UserRepresentation> getUsers(@QueryParam("search") String search,
@QueryParam("lastName") String last,
@QueryParam("firstName") String first,
@QueryParam("email") String email,
@QueryParam("username") String username,
@QueryParam("first") Integer firstResult,
@QueryParam("max") Integer maxResults) {
auth.requireView();
firstResult = firstResult != null ? firstResult : -1;
maxResults = maxResults != null ? maxResults : Constants.DEFAULT_MAX_RESULTS;
List<UserRepresentation> results = new ArrayList<UserRepresentation>();
List<UserModel> userModels;
if (search != null) {
userModels = session.users().searchForUser(search.trim(), realm, firstResult, maxResults);
} else if (last != null || first != null || email != null || username != null) {
Map<String, String> attributes = new HashMap<String, String>();
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);
}
userModels = session.users().searchForUser(attributes, realm, firstResult, maxResults);
} else {
userModels = session.users().getUsers(realm, firstResult, maxResults, false);
}
for (UserModel user : userModels) {
results.add(ModelToRepresentation.toRepresentation(session, realm, user));
}
return results;
}
@Path("count")
@GET
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public Integer getUsersCount() {
auth.requireView();
return session.users().getUsersCount(realm);
}
@Path("{id}/role-mappings")
public RoleMapperResource getRoleMappings(@PathParam("id") String id) {
auth.init(RealmAuth.Resource.USER);
UserModel user = session.users().getUserById(id, realm);
RoleMapperResource resource = new RoleMapperResource(realm, auth, user, adminEvent);
ResteasyProviderFactory.getInstance().injectProperties(resource);
return resource;
}
/**
* Disable all credentials for a user of a specific type
*
* @param id
* @param credentialTypes
*/
@Path("{id}/disable-credential-types")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void disableCredentialType(@PathParam("id") String id, List<String> credentialTypes) {
auth.requireManage();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
if (credentialTypes == null) return;
for (String type : credentialTypes) {
session.userCredentialManager().disableCredentialType(realm, user, type);
}
}
/**
* Set up a temporary password for the user
*
* User will have to reset the temporary password next time they log in.
*
* @param id User id
* @param pass A Temporary password
*/
@Path("{id}/reset-password")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void resetPassword(@PathParam("id") String id, CredentialRepresentation pass) {
auth.requireManage();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
if (pass == null || pass.getValue() == null || !CredentialRepresentation.PASSWORD.equals(pass.getType())) {
throw new BadRequestException("No password provided");
}
if (Validation.isBlank(pass.getValue())) {
throw new BadRequestException("Empty password not allowed");
}
UserCredentialModel cred = UserCredentialModel.password(pass.getValue(), true);
try {
session.userCredentialManager().updateCredential(realm, user, cred);
} catch (IllegalStateException ise) {
throw new BadRequestException("Resetting to N old passwords is not allowed.");
} catch (ReadOnlyException mre) {
throw new BadRequestException("Can't reset password as account is read only");
} catch (ModelException e) {
Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale());
throw new ErrorResponseException(e.getMessage(), MessageFormat.format(messages.getProperty(e.getMessage(), e.getMessage()), e.getParameters()),
Status.BAD_REQUEST);
}
if (pass.isTemporary() != null && pass.isTemporary()) user.addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD);
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
}
/**
* Remove TOTP from the user
*
* @param id User id
*/
@Path("{id}/remove-totp")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public void removeTotp(@PathParam("id") String id) {
auth.requireManage();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
session.userCredentialManager().disableCredentialType(realm, user, CredentialModel.OTP);
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
}
/**
* Send an email to the user with a link they can click to reset their password.
* The redirectUri and clientId parameters are optional. The default for the
* redirect is the account client.
*
* This endpoint has been deprecated. Please use the execute-actions-email passing a list with
* UPDATE_PASSWORD within it.
*
* @param id
* @param redirectUri redirect uri
* @param clientId client id
* @return
*/
@Deprecated
@Path("{id}/reset-password-email")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public Response resetPasswordEmail(@PathParam("id") String id,
@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri,
@QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) {
List<String> actions = new LinkedList<>();
actions.add(UserModel.RequiredAction.UPDATE_PASSWORD.name());
return executeActionsEmail(id, redirectUri, clientId, null, actions);
}
/**
* Send a update account email to the user
*
* An email contains a link the user can click to perform a set of required actions.
* The redirectUri and clientId parameters are optional. If no redirect is given, then there will
* be no link back to click after actions have completed. Redirect uri must be a valid uri for the
* particular clientId.
*
* @param id User is
* @param redirectUri Redirect uri
* @param clientId Client id
* @param lifespan Number of seconds after which the generated token expires
* @param actions required actions the user needs to complete
* @return
*/
@Path("{id}/execute-actions-email")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public Response executeActionsEmail(@PathParam("id") String id,
@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri,
@QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId,
@QueryParam("lifespan") Integer lifespan,
List<String> actions) {
auth.requireManage();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
return ErrorResponse.error("User not found", Response.Status.NOT_FOUND);
}
if (user.getEmail() == null) {
return ErrorResponse.error("User email missing", Response.Status.BAD_REQUEST);
}
if (!user.isEnabled()) {
throw new WebApplicationException(
ErrorResponse.error("User is disabled", Response.Status.BAD_REQUEST));
}
if (redirectUri != null && clientId == null) {
throw new WebApplicationException(
ErrorResponse.error("Client id missing", Response.Status.BAD_REQUEST));
}
if (clientId == null) {
clientId = Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
}
ClientModel client = realm.getClientByClientId(clientId);
if (client == null || !client.isEnabled()) {
throw new WebApplicationException(
ErrorResponse.error(clientId + " not enabled", Response.Status.BAD_REQUEST));
}
String redirect;
if (redirectUri != null) {
redirect = RedirectUtils.verifyRedirectUri(uriInfo, redirectUri, realm, client);
if (redirect == null) {
throw new WebApplicationException(
ErrorResponse.error("Invalid redirect uri.", Response.Status.BAD_REQUEST));
}
}
if (lifespan == null) {
lifespan = realm.getActionTokenGeneratedByAdminLifespan();
}
int expiration = Time.currentTime() + lifespan;
ExecuteActionsActionToken token = new ExecuteActionsActionToken(id, expiration, actions, redirectUri, clientId);
try {
UriBuilder builder = LoginActionsService.actionTokenProcessor(uriInfo);
builder.queryParam("key", token.serialize(session, realm, uriInfo));
String link = builder.build(realm.getName()).toString();
this.session.getProvider(EmailTemplateProvider.class)
.setRealm(realm)
.setUser(user)
.sendExecuteActions(link, TimeUnit.SECONDS.toMinutes(lifespan));
//audit.user(user).detail(Details.EMAIL, user.getEmail()).detail(Details.CODE_ID, accessCode.getCodeId()).success();
adminEvent.operation(OperationType.ACTION).resourcePath(uriInfo).success();
return Response.ok().build();
} catch (EmailException e) {
ServicesLogger.LOGGER.failedToSendActionsEmail(e);
return ErrorResponse.error("Failed to send execute actions email", Response.Status.INTERNAL_SERVER_ERROR);
}
}
/**
* Send an email-verification email to the user
*
* An email contains a link the user can click to verify their email address.
* The redirectUri and clientId parameters are optional. The default for the
* redirect is the account client.
*
* @param id User id
* @param redirectUri Redirect uri
* @param clientId Client id
* @return
*/
@Path("{id}/send-verify-email")
@PUT
@Consumes(MediaType.APPLICATION_JSON)
public Response sendVerifyEmail(@PathParam("id") String id, @QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String redirectUri, @QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId) {
List<String> actions = new LinkedList<>();
actions.add(UserModel.RequiredAction.VERIFY_EMAIL.name());
return executeActionsEmail(id, redirectUri, clientId, null, actions);
}
@GET
@Path("{id}/groups")
@NoCache
@Produces(MediaType.APPLICATION_JSON)
public List<GroupRepresentation> groupMembership(@PathParam("id") String id) {
auth.requireView();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
List<GroupRepresentation> memberships = new LinkedList<>();
for (GroupModel group : user.getGroups()) {
memberships.add(ModelToRepresentation.toRepresentation(group, false));
}
return memberships;
}
@DELETE
@Path("{id}/groups/{groupId}")
@NoCache
public void removeMembership(@PathParam("id") String id, @PathParam("groupId") String groupId) {
auth.requireManage();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
GroupModel group = session.realms().getGroupById(groupId, realm);
if (group == null) {
throw new NotFoundException("Group not found");
}
try {
if (user.isMemberOf(group)){
user.leaveGroup(group);
adminEvent.operation(OperationType.DELETE).resource(ResourceType.GROUP_MEMBERSHIP).representation(ModelToRepresentation.toRepresentation(group, true)).resourcePath(uriInfo).success();
}
} catch (ModelException me) {
Properties messages = AdminRoot.getMessages(session, realm, auth.getAuth().getToken().getLocale());
throw new ErrorResponseException(me.getMessage(), MessageFormat.format(messages.getProperty(me.getMessage(), me.getMessage()), me.getParameters()),
Response.Status.BAD_REQUEST);
}
}
@PUT
@Path("{id}/groups/{groupId}")
@NoCache
public void joinGroup(@PathParam("id") String id, @PathParam("groupId") String groupId) {
auth.requireManage();
UserModel user = session.users().getUserById(id, realm);
if (user == null) {
throw new NotFoundException("User not found");
}
GroupModel group = session.realms().getGroupById(groupId, realm);
if (group == null) {
throw new NotFoundException("Group not found");
}
if (!user.isMemberOf(group)){
user.joinGroup(group);
adminEvent.operation(OperationType.CREATE).resource(ResourceType.GROUP_MEMBERSHIP).representation(ModelToRepresentation.toRepresentation(group, true)).resourcePath(uriInfo).success();
}
}
}