idm-service/src/main/java/org/gcube/service/idm/rest/UserAPI.java

503 lines
19 KiB
Java

package org.gcube.service.idm.rest;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.gcube.common.security.Owner;
import org.gcube.common.security.providers.SecretManagerProvider;
import org.gcube.common.security.secrets.Secret;
import org.gcube.service.idm.IdMManager;
import org.gcube.service.idm.controller.AuthController;
import org.gcube.service.idm.controller.JWTController;
import org.gcube.service.idm.controller.KCUserController;
import org.gcube.service.idm.controller.LiferayProfileClient;
import org.gcube.service.idm.keycloack.KkClientFactory;
import org.gcube.service.idm.serializers.IdmObjectSerializator;
import org.gcube.service.rest.ErrorMessages;
import org.gcube.service.rest.ResponseBean;
import org.gcube.service.rest.ResponseBeanMap;
import org.gcube.service.rest.ResponseBeanPaginated;
import org.gcube.smartgears.annotations.ManagedBy;
import org.gcube.vomanagement.usermanagement.model.GCubeUser;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.representations.idm.GroupRepresentation;
import org.keycloak.representations.idm.MappingsRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.webcohesion.enunciate.metadata.rs.RequestHeader;
import com.webcohesion.enunciate.metadata.rs.RequestHeaders;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.DefaultValue;
import jakarta.ws.rs.ForbiddenException;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
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.MediaType;
import jakarta.ws.rs.core.Response;
/**
* <p>
* The REST API to interact with the keycloak users
* </p>
*
* <p>
* "Member" users can only invoke metods on his own user (/me)
* </p>
*
* <p>
* Users with "idm-service-read" or "idm-service-admin" role can invoke metods
* on all the users
* </p>
*
* @author Alfredo Oliviero (ISTI - CNR)
*/
@ManagedBy(IdMManager.class)
@RequestHeaders({
@RequestHeader(name = "Authorization", description = "Bearer token, see https://dev.d4science.org/how-to-access-resources"),
@RequestHeader(name = "Content-Type", description = "application/json")
})
@Path("users")
public class UserAPI {
private static final org.slf4j.Logger logger = LoggerFactory.getLogger(UserAPI.class);
/**
* Returns infos about the authenticated user
*
* <ul>
* <li>owner: the authenticated user</li>
* <li>profile: the profile of the user in the Liferay CMS (only if the user is
* not a service)</li>
* <li>user: the user representation from the authentication service</li>
* </uL>
*
* if the optional parameter inspect is passed as true, returns additional
* values:
* <ul>
* <li>verify: the result of introspection of the auth token on the
* authentication service</li>
* <li>roles: the authenticated user</li>
* <li>groups: the authenticated user</li>
* <li>groupRolesRealm: ...</li>
* <li>groupRolesClients: ...</li>
* </ul>
*
* @param inspect adds additional inspection values to the result
* @returns infos about the authenticated user
*
*/
@GET
@Path("/me")
@Produces(MediaType.APPLICATION_JSON)
@StatusCodes({
@ResponseCode(code = 200, condition = "current user informations"),
@ResponseCode(code = 403, condition = ErrorMessages.NOT_USER_TOKEN_CONTEXT_USED),
@ResponseCode(code = 404, condition = ErrorMessages.INVALID_ATTRIBUTE),
@ResponseCode(code = 500, condition = ErrorMessages.ERROR_IN_API_RESULT)
})
public Response getMe(
@QueryParam("inspect") @DefaultValue("false") Boolean inspect) {
logger.info("/users/me");
ResponseBean responseBean = new ResponseBean();
ObjectMapper objectMapper = IdmObjectSerializator.getSerializer();
Secret secret = SecretManagerProvider.get();
Owner owner = secret.getOwner();
String username = owner.getId();
// if (owner.isApplication()) {
// // only users can use "me"
// throw new ForbiddenException(ErrorMessages.NOT_USER_TOKEN_CONTEXT_USED);
// }
Map<String, Object> result = getUserData(username, !owner.isApplication(), inspect);
responseBean.setResult(result);
result.put("owner", owner);
try {
if (inspect) {
String token = AuthController.getAccessToken();
result.put("verify", JWTController.decodeJwtToken(token));
}
responseBean.setSuccess(true);
String jsonData = objectMapper.writeValueAsString(responseBean);
return Response.ok(jsonData).build();
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new InternalServerErrorException(e);
}
}
/**
* Returns informations about the user received as parameter
*
* Only users with "idm-service-read" or "idm-service-admin" role can invoke
* this method
*
* <ul>
* <li>profile: the profile of the user in the Liferay CMS (only if the user is
* not a service)</li>
* <li>user: the user representation from the authentication service</li>
* </uL>
*
* if the optional parameter inspect is passed as true, returns additional
* values:
* <ul>
* <li>roles: the authenticated user</li>
* <li>groups: the authenticated user</li>
* <li>groupRolesRealm: ...</li>
* <li>groupRolesClients: ...</li>
* </ul>
*
* @param username the username of the user
* @param inspect adds additional inspection values to the result
* @returns infos about the user
*/
@GET
@Path("/{username}")
@Produces(MediaType.APPLICATION_JSON)
@StatusCodes({
@ResponseCode(code = 200, condition = "user informations"),
@ResponseCode(code = 403, condition = ErrorMessages.NOT_USER_TOKEN_CONTEXT_USED),
@ResponseCode(code = 404, condition = ErrorMessages.INVALID_ATTRIBUTE),
@ResponseCode(code = 500, condition = ErrorMessages.ERROR_IN_API_RESULT)
})
public Response getUser(
@PathParam("username") String username,
@QueryParam("inspect") @DefaultValue("false") Boolean inspect) {
ResponseBeanMap responseBean = new ResponseBeanMap();
ObjectMapper objectMapper = IdmObjectSerializator.getSerializer();
if (!AuthController.checkAnyRole(AuthController.ACCESS_READ_ROLES)) {
// the user can see only his profile
throw new ForbiddenException(ErrorMessages.USER_NOT_AUTHORIZED_PRIVATE);
}
Secret secret = SecretManagerProvider.get();
Owner owner = secret.getOwner();
try {
Map<String, Object> result = getUserData(username, !owner.isApplication(), inspect);
responseBean.setResult(result);
responseBean.setSuccess(true);
String jsonData = objectMapper.writeValueAsString(responseBean);
return Response.ok(jsonData).build();
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new InternalServerErrorException(e);
}
}
protected Map<String, Object> getUserData(String username, Boolean getProfile, Boolean isInspect) {
Map<String, Object> result = new HashMap<>();
UserRepresentation user = KCUserController.getUserByUsername(username);
result.put("user", user);
if (getProfile) {
GCubeUser profile = LiferayProfileClient.getUserProfileByUsername(username);
result.put("profile", profile);
// throw new ForbiddenException(ErrorMessages.NOT_USER_TOKEN_CONTEXT_USED);
}
if (isInspect) {
UserResource userResource = KCUserController.getUserResourceByUsername(username);
MappingsRepresentation roles = userResource.roles().getAll();
result.put("roles", roles);
List<GroupRepresentation> groups = userResource.groups();
result.put("groups", groups);
HashMap<String, Object> groupRolesRealm = new HashMap<String, Object>();
HashMap<String, Object> groupRolesClients = new HashMap<String, Object>();
result.put("groupRolesRealm", groupRolesRealm);
result.put("groupRolesClients", groupRolesClients);
for (GroupRepresentation g : groups) {
groupRolesClients.put(g.getId(), g.getClientRoles());
}
}
return result;
}
/**
* @returns the owner object of the authenticated user
*/
@GET
@Path("/me/owner")
@Produces({ "application/json;charset=UTF-8", "application/vnd.api+json" })
@StatusCodes({
@ResponseCode(code = 200, condition = "infos about the owner of the auth token"),
@ResponseCode(code = 403, condition = ErrorMessages.NOT_USER_TOKEN_CONTEXT_USED),
@ResponseCode(code = 404, condition = ErrorMessages.INVALID_ATTRIBUTE),
@ResponseCode(code = 500, condition = ErrorMessages.ERROR_IN_API_RESULT)
})
public Response getCurrentUser() {
ResponseBean responseBean = new ResponseBean();
Secret secret = SecretManagerProvider.get();
Owner owner = secret.getOwner();
try {
// UserResource user = KCUserController.getUserById();
responseBean.setResult(owner);
responseBean.setSuccess(true);
ObjectMapper objectMapper = IdmObjectSerializator.getSerializer();
String jsonData = objectMapper.writeValueAsString(responseBean);
return Response.ok(jsonData).build();
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new InternalServerErrorException(e);
}
}
/**
* @returns the result of introspection of the auth token on the
* authentication service
*/
@GET
@Path("/me/verify")
@Produces({ "application/json;charset=UTF-8", "application/vnd.api+json" })
@StatusCodes({
@ResponseCode(code = 200, condition = "decode the token"),
@ResponseCode(code = 403, condition = ErrorMessages.NOT_USER_TOKEN_CONTEXT_USED),
@ResponseCode(code = 404, condition = ErrorMessages.INVALID_ATTRIBUTE),
@ResponseCode(code = 500, condition = ErrorMessages.ERROR_IN_API_RESULT)
})
public Response getInrospectioCurrenttUser() {
ResponseBean responseBean = new ResponseBean();
try {
String token = AuthController.getAccessToken();
responseBean.setResult(JWTController.decodeJwtToken(token));
// UserResource user = KCUserController.getUserById();
responseBean.setSuccess(true);
ObjectMapper objectMapper = IdmObjectSerializator.getSerializer();
String jsonData = objectMapper.writeValueAsString(responseBean);
return Response.ok(jsonData).build();
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new InternalServerErrorException(e);
}
}
/**
* Returns informations about the user received as parameter (can be also me)
*
* Normal member users can use only "me" or his username as parameter
*
* Users with "idm-service-read" or "idm-service-admin" role can invoke
* this method with any username
*
* accepted parameters are:
*
* <ul>
* <li>profile: returns the profile of the user in the Liferay CMS (only if the
* user is
* not a service)</li>
* <li>email: returns the email of the user from the authentication service</li>
*
* <li>user: the user representation from the authentication service</li>
* </uL>
*
* if the optional parameter inspect is passed as true, returns additional
* values:
* <ul>
* <li>roles_realm: roles in realm for the user from the authentication
* service</li>
* <li>roles_clients: roles in clients for the user from the authentication
* service</li>
* <li>groups: id of the user from the authentication service</li>
* <li>username: username of the user from the authentication service</li>
* <li>name: Fullname of the user from the authentication service</li>
* <li>attributes: attributes of the user from the authentication service</li>
* <li>user: full user from the authentication service</li>
* <li>profile: profile of the user from the Liferay CMS service</li>
* </ul>
*
* @param username the username of the user
* @param parameter the parameter to obtain. accepts profile, email,
* roles_realm, roles_clients, groups, id, username , name,
* attributes, user
* @param inspect adds additional inspection values to the result
* @returns infos about the user
*/
public enum USER_DETAILS {
profile, email, roles_realm, roles_clients,
groups, id, username, name, attributes, user
}
@GET
@Path("/{username}/{parameter}")
@Produces(MediaType.APPLICATION_JSON)
@StatusCodes({
@ResponseCode(code = 200, condition = "decode the token"),
@ResponseCode(code = 403, condition = ErrorMessages.NOT_USER_TOKEN_CONTEXT_USED),
@ResponseCode(code = 404, condition = ErrorMessages.INVALID_ATTRIBUTE),
@ResponseCode(code = 500, condition = ErrorMessages.ERROR_IN_API_RESULT)
})
public Response getUserParameter(
@PathParam("username") String username,
@PathParam("parameter") USER_DETAILS parameter) {
ResponseBean responseBean = new ResponseBean();
ObjectMapper objectMapper = IdmObjectSerializator.getSerializer();
Secret secret = SecretManagerProvider.get();
Owner owner = secret.getOwner();
if (username.equals("me")) {
username = owner.getId();
}
if (!AuthController.checkAnyRole(AuthController.ACCESS_READ_ROLES)
&& !AuthController.userIsMe(username, owner)) {
// the user can see only his profile
throw new ForbiddenException(ErrorMessages.USER_NOT_AUTHORIZED_PRIVATE);
}
try {
if (parameter.equals(USER_DETAILS.profile)) {
GCubeUser profile = LiferayProfileClient.getUserProfileByUsername(username);
responseBean.setResult(profile);
String jsonData = objectMapper.writeValueAsString(responseBean);
return Response.ok(jsonData).build();
}
UserRepresentation user = KCUserController.getUserByUsername(username);
if (parameter.equals(USER_DETAILS.email))
responseBean.setResult(user.getEmail());
else if (parameter.equals(USER_DETAILS.roles_realm))
responseBean.setResult(user.getRealmRoles());
else if (parameter.equals(USER_DETAILS.roles_clients))
responseBean.setResult(user.getClientRoles());
else if (parameter.equals(USER_DETAILS.groups))
responseBean.setResult(user.getGroups());
else if (parameter.equals(USER_DETAILS.id))
responseBean.setResult(user.getId());
else if (parameter.equals(USER_DETAILS.username))
responseBean.setResult(user.getUsername());
else if (parameter.equals(USER_DETAILS.name))
responseBean.setResult(user.getFirstName() + " " + user.getLastName());
else if (parameter.equals(USER_DETAILS.attributes))
responseBean.setResult(user.getAttributes());
else if (parameter.equals(USER_DETAILS.user) || parameter == null)
responseBean.setResult(user);
else
throw new BadRequestException("unknow parameter " + parameter);
responseBean.setSuccess(true);
String jsonData = objectMapper.writeValueAsString(responseBean);
return Response.ok(jsonData).build();
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new InternalServerErrorException(e);
}
}
/**
* Search users in all realm, filtered according to query parameters.
*
* @param format response format
* @param exact Boolean which defines whether the params 'last', 'first',
* 'email' and 'username' must match exactly. default true
* @param username A String contained in username, or the complete username,
* if
* param 'exact' is true
* @param firstName A String contained in firstName, or the complete
* firstName,
* if param 'exact' is true
* @param lastName A String contained in firstName, or the complete
* firstName,
* if param 'exact' is true
* @param firstResult pagination offset
* @param maxResults maximum results size
* @param enabled Boolean representing if user is enabled or not
* @param email A String contained in email, or the complete email, if
* param 'exact' is true
*/
@GET
@Path("/search")
@Produces(MediaType.APPLICATION_JSON)
public Response search(
@QueryParam("format") @DefaultValue("username") KCUserController.REPR format,
@QueryParam("exact") @DefaultValue("true") Boolean exact,
@QueryParam("username") String username,
@QueryParam("firsnName") String firstName,
@QueryParam("lastName") String lastName,
@QueryParam("email") String email,
@QueryParam("first") @DefaultValue("0") int firstResult,
@QueryParam("max") @DefaultValue("100") int maxResults,
@QueryParam("enabled") @DefaultValue("true") Boolean enabled) {
ResponseBean responseBean;
responseBean = new ResponseBeanPaginated(firstResult, maxResults);
try {
if (!format.equals(KCUserController.REPR.username)
&& !AuthController.checkAnyRole(AuthController.ACCESS_READ_ROLES)) {
// the user can see only his profile
throw new ForbiddenException(ErrorMessages.USER_NOT_AUTHORIZED_PRIVATE);
}
RealmResource realm = KkClientFactory.getSingleton().getKKRealm();
Boolean briefRepresentation = !KCUserController.REPR.full.equals(format);
List<UserRepresentation> users = realm.users().search(
username, firstName, lastName, email,
firstResult, maxResults,
enabled, briefRepresentation, exact);
responseBean.setResult(KCUserController.formatList(users, format));
responseBean.setSuccess(true);
ObjectMapper objectMapper = IdmObjectSerializator.getSerializer();
String jsonData = objectMapper.writeValueAsString(responseBean);
return Response.ok(jsonData).build();
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new InternalServerErrorException(e);
}
}
}