karso/back/src/org/kar/karso/api/UserResource.java
Edouard DUPIN faf60ed89b [FIX] many correction:
- missing some @Produces
  - update logger in static
  - some coding style
  - Some error in code
2024-05-09 00:20:18 +02:00

349 lines
12 KiB
Java
Executable File

package org.kar.karso.api;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.kar.archidata.dataAccess.DataAccess;
import org.kar.archidata.dataAccess.QueryCondition;
import org.kar.archidata.dataAccess.addOn.AddOnManyToMany;
import org.kar.archidata.dataAccess.options.Condition;
import org.kar.archidata.exception.FailException;
import org.kar.archidata.exception.InputException;
import org.kar.archidata.exception.SystemException;
import org.kar.archidata.filter.GenericContext;
import org.kar.archidata.model.GetToken;
import org.kar.archidata.tools.JWTWrapper;
import org.kar.karso.migration.Initialization;
import org.kar.karso.model.ChangePassword;
import org.kar.karso.model.DataGetToken;
import org.kar.karso.model.UserAuth;
import org.kar.karso.model.UserAuthGet;
import org.kar.karso.model.UserCreate;
import org.kar.karso.util.ConfigVariable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.fasterxml.jackson.annotation.JsonInclude;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.PATCH;
import jakarta.ws.rs.POST;
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.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
@Path("/users")
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {
private static final Logger LOGGER = LoggerFactory.getLogger(UserResource.class);
@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserOut {
public long id;
public String login;
public UserOut(final long id, final String login) {
this.id = id;
this.login = login;
}
}
public UserResource() {}
@GET
@RolesAllowed("ADMIN")
public List<UserAuthGet> getUsers() throws Exception {
return DataAccess.gets(UserAuthGet.class);
}
@GET
@Path("{id}")
@RolesAllowed("ADMIN")
public UserAuthGet getUser(@Context final SecurityContext sc, @PathParam("id") final long userId) throws Exception {
//GenericContext gc = (GenericContext) sc.getUserPrincipal();
return DataAccess.get(UserAuthGet.class, userId);
}
@POST
@Path("{userId}/application/{applicationId}/link")
@RolesAllowed("ADMIN")
public UserAuth linkApplication(
@Context final SecurityContext sc,
@PathParam("userId") final long userId,
@PathParam("applicationId") final long applicationId,
final boolean data) throws Exception {
LOGGER.debug("Find typeNode");
if (data) {
AddOnManyToMany.addLink(UserAuth.class, userId, "application", applicationId);
} else {
AddOnManyToMany.removeLink(UserAuth.class, userId, "application", applicationId);
}
return DataAccess.get(UserAuth.class, userId);
}
@GET
@Path("{userId}/application/{applicationId}/rights")
@RolesAllowed("ADMIN")
public Map<String, Object> getApplicationRight(
@Context final SecurityContext sc,
@PathParam("userId") final long userId,
@PathParam("applicationId") final long applicationId) throws Exception {
return RightResource.getUserRight(userId, applicationId);
}
@PATCH
@Path("{userId}/application/{applicationId}/rights")
@RolesAllowed("ADMIN")
@Consumes(MediaType.APPLICATION_JSON)
public Map<String, Object> patchApplicationRight(
@Context final SecurityContext sc,
@PathParam("userId") final long userId,
@PathParam("applicationId") final long applicationId,
final Map<String, Object> data) throws Exception {
this.LOGGER.info("Patch data from FRONT: {}", data);
RightResource.updateUserRight(userId, applicationId, data);
return RightResource.getUserRight(userId, applicationId);
}
// TODO: check this it might be deprecated ...
@POST
@Path("{id}/set_admin")
@RolesAllowed("ADMIN")
@Consumes(MediaType.APPLICATION_JSON)
public void setAdmin(@Context final SecurityContext sc, @PathParam("id") final long userId, final boolean data)
throws Exception {
final UserAuth user = new UserAuth();
user.admin = data;
final int ret = DataAccess.update(user, userId, List.of("admin"));
if (ret == 0) {
throw new FailException(Response.Status.NOT_MODIFIED, "Fail to modify user as an admin.");
}
}
@POST
@Path("{id}/set_blocked")
@RolesAllowed("ADMIN")
@Consumes(MediaType.APPLICATION_JSON)
public void setBlocked(@Context final SecurityContext sc, @PathParam("id") final long userId, final boolean data)
throws Exception {
final UserAuth user = new UserAuth();
user.blocked = data;
final int ret = DataAccess.update(user, userId, List.of("blocked"));
if (ret == 0) {
throw new FailException(Response.Status.NOT_MODIFIED, "Fail to block the User.");
}
}
@POST
@RolesAllowed("ADMIN")
@Consumes(MediaType.APPLICATION_JSON)
public UserAuthGet create(final UserCreate user) throws Exception {
LOGGER.debug("create new User email={} login={}", user.email, user.login);
// verify login or email is correct:
if (user.login == null || user.login.length() < 6) {
throw new InputException("login", "Authentiocate-method-error (login too small: '" + user.login + "')");
}
// TODO: check login format
if (user.email == null || user.email.length() < 6) {
throw new InputException("email", "Authentiocate-method-error (email too small: '" + user.email + "')");
}
// TODO: check email format
if (user.password == null || user.password.length() != 128) {
throw new InputException("password", "null password, or wrong hash size");
}
// TODO: verify if the data are a hash ...
// Check login does not exist
List<UserAuth> out = DataAccess.getsWhere(UserAuth.class,
new Condition(new QueryCondition("login", "=", user.login)));
if (out.size() >= 1) {
throw new FailException(Response.Status.BAD_REQUEST, "Login already used !!!");
}
// Check email does not exist
out = DataAccess.getsWhere(UserAuth.class, new Condition(new QueryCondition("email", "=", user.email)));
if (out.size() >= 1) {
throw new FailException(Response.Status.BAD_REQUEST, "e-mail already used !!!");
}
// Add new user and return formated data.
final UserAuth newUser = new UserAuth();
newUser.admin = false;
newUser.removed = false;
newUser.blocked = false;
newUser.avatar = false;
newUser.login = user.login;
newUser.password = user.password;
newUser.email = user.email;
newUser.lastConnection = Timestamp.valueOf(LocalDateTime.now());
final UserAuth tmp = DataAccess.insert(newUser);
LOGGER.debug("create new user done with id=={}", tmp.id);
return DataAccess.get(UserAuthGet.class, tmp.id);
}
@GET
@Path("me")
@RolesAllowed("USER")
public UserOut getMe(@Context final SecurityContext sc) {
LOGGER.debug("getMe()");
final GenericContext gc = (GenericContext) sc.getUserPrincipal();
LOGGER.debug("== USER ? {}", gc.userByToken);
return new UserOut(gc.userByToken.id, gc.userByToken.name);
}
@POST
@Path("password")
@RolesAllowed("USER")
@Consumes(MediaType.APPLICATION_JSON)
public void changePassword(@Context final SecurityContext sc, final ChangePassword data) throws Exception {
LOGGER.debug("ChangePassword()");
final GenericContext gc = (GenericContext) sc.getUserPrincipal();
LOGGER.debug("== USER ? {}", gc.userByToken);
if (data == null) {
throw new InputException("data", "No data set...");
}
if (data.newPassword == null || data.newPassword.length() != 128) {
throw new InputException("newPassword", "null password, or wrong hash size");
}
final UserAuth user = checkAuthUser(data.method, data.login, data.time, data.password);
if (user == null) {
throw new SystemException("Fail to retrieve the user... (impossible case)");
}
// Process the update:
user.password = data.newPassword;
DataAccess.update(user, user.id, List.of("password"));
}
@GET
@Path("is_login_exist")
@PermitAll
public Boolean isLoginExist(@QueryParam("login") final String login) throws Exception {
LOGGER.debug("checkLogin: '{}'", login);
final List<UserAuth> out = DataAccess.getsWhere(UserAuth.class,
new Condition(new QueryCondition("login", "=", login)));
return out.size() >= 1;
}
// TODO: add an application TOKEN and permit only 50 requested (maybe add an option to disable it).
@GET
@Path("is_email_exist")
@PermitAll
public Boolean isEmailExist(@QueryParam("email") final String email) throws Exception {
LOGGER.debug("checkEmail: {}", email);
final List<UserAuth> out = DataAccess.getsWhere(UserAuth.class,
new Condition(new QueryCondition("email", "=", email)));
return out.size() >= 1;
}
private UserAuth checkAuthUser(final String method, final String login, final String time, final String password)
throws Exception {
// check good version:
if (!"v1".contentEquals(method)) {
throw new InputException("method", "Authentiocate-method-error (wrong version: '" + method + "')");
}
// verify login or email is correct:
if (login.length() < 6) {
throw new InputException("login", "Authentiocate-method-error (email or login too small: '" + login + "')");
}
if (password == null || password.length() != 128) {
throw new InputException("password", "null password, or wrong hash size");
}
// email or login?
String query = "login";
if (login.contains("@")) {
query = "email";
}
final UserAuth user = DataAccess.getWhere(UserAuth.class, new Condition(new QueryCondition(query, "=", login)));
if (user == null) {
throw new FailException(Response.Status.PRECONDITION_FAILED,
"FAIL Authentiocate-wrong email/login '" + login + "')");
}
// Check the password:
final String passwodCheck = getSHA512("login='" + login + "';pass='" + user.password + "';date='" + time + "'");
if (!passwodCheck.contentEquals(password)) {
throw new FailException(Response.Status.PRECONDITION_FAILED, "Password error ...");
}
LOGGER.debug(" ==> pass nearly all test : admin={} blocked={} removed={}", user.admin, user.blocked,
user.removed);
if (user.blocked || user.removed) {
throw new FailException(Response.Status.UNAUTHORIZED, "FAIL Authentiocate");
}
return user;
}
@POST
@Path("get_token")
@PermitAll
@Consumes(MediaType.APPLICATION_JSON)
public GetToken getToken(final DataGetToken data) throws Exception {
LOGGER.info("User Authenticate: {}", data.login());
final UserAuth user = checkAuthUser(data.method(), data.login(), data.time(), data.password());
// at the point the user has been not deleted and not blocked.
// this authentication is valid only for Karso ==> not for the application
final int expirationTimeInMinutes = ConfigVariable.getAuthExpirationTime();
// Get the USER Right (Note: by construction KARSO have application ID = KARSO_INITIALISATION_ID
final Map<String, Object> ssoRight = RightResource.getUserRight(user.id,
Initialization.KARSO_INITIALISATION_ID);
if (!ssoRight.containsKey("USER")) {
// If the USER is not override, the system add by default USER
ssoRight.put("USER", true);
}
LOGGER.debug("Get new token with right: {}", ssoRight);
final Map<String, Object> outRight = new HashMap<>();
final String applicationName = "karso";
// we set the right in the under map to manage multiple application group right. and in some application user can see other user or all user of the application
outRight.put(applicationName, ssoRight);
// TODO: maybe correct this get of TTL...
final String ret = JWTWrapper.generateJWToken(user.id, user.login, "KarAuth", applicationName, outRight,
expirationTimeInMinutes);
if (ret == null) {
throw new SystemException("Missing internal JWT system ==> can not sign anything ...");
}
// Update last connection:
final UserAuth newUser = new UserAuth();
newUser.lastConnection = Timestamp.valueOf(LocalDateTime.now());
DataAccess.update(newUser, user.id, List.of("lastConnection"));
//LOGGER.debug(" ==> generate token: {}", ret);
return new GetToken(ret);
}
public static String bytesToHex(final byte[] bytes) {
final StringBuilder sb = new StringBuilder();
for (final byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
public String getSHA512(final String passwordToHash) {
try {
final MessageDigest md = MessageDigest.getInstance("SHA-512");
final byte[] bytes = md.digest(passwordToHash.getBytes(StandardCharsets.UTF_8));
return bytesToHex(bytes);
} catch (final NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
}