added security to picture upload resource
This commit is contained in:
@@ -0,0 +1,116 @@
|
|||||||
|
package marketing.heyday.hartmann.fotodocumentation.core.utils;
|
||||||
|
|
||||||
|
import java.nio.charset.Charset;
|
||||||
|
import java.security.Principal;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.apache.commons.codec.binary.Base64;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.wildfly.security.auth.principal.NamePrincipal;
|
||||||
|
import org.wildfly.security.auth.server.RealmUnavailableException;
|
||||||
|
import org.wildfly.security.auth.server.SecurityDomain;
|
||||||
|
import org.wildfly.security.auth.server.SecurityIdentity;
|
||||||
|
import org.wildfly.security.evidence.PasswordGuessEvidence;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import javaslang.Tuple;
|
||||||
|
import javaslang.Tuple2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* <p>Copyright: Copyright (c) 2024</p>
|
||||||
|
* <p>Company: heyday Marketing GmbH</p>
|
||||||
|
* @author <a href="mailto:p.verboom@heyday.marketing">Patrick Verboom</a>
|
||||||
|
* @version 1.0
|
||||||
|
*
|
||||||
|
* created: 20 Jan 2026
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class LoginUtils {
|
||||||
|
private static final Log LOG = LogFactory.getLog(LoginUtils.class);
|
||||||
|
private static final int BASIC_HEADER_SIZE = 6;
|
||||||
|
private static final int USERPASS_LENGTH = 1;
|
||||||
|
|
||||||
|
public Optional<SecurityIdentity> authenticate(HttpServletRequest httpServletRequest) {
|
||||||
|
Tuple2<String, String> userPass = extractUsernamePassword(httpServletRequest);
|
||||||
|
if (userPass._1.isBlank() || userPass._2.isBlank()) {
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
return authenticate(userPass._1, userPass._2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Optional<String> getSecurityToken(HttpServletRequest httpServletRequest) {
|
||||||
|
Optional<String[]> headerOpt = extractAuthHeader(httpServletRequest);
|
||||||
|
if (headerOpt.isPresent() && headerOpt.get().length > 2) {
|
||||||
|
return Optional.ofNullable(headerOpt.get()[2]);
|
||||||
|
}
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<SecurityIdentity> authenticate(String username, String password) {
|
||||||
|
try {
|
||||||
|
Principal principal = new NamePrincipal(username);
|
||||||
|
PasswordGuessEvidence evidence = new PasswordGuessEvidence(password.toCharArray());
|
||||||
|
|
||||||
|
SecurityDomain sd = SecurityDomain.getCurrent();
|
||||||
|
return Optional.ofNullable(sd.authenticate(principal, evidence));
|
||||||
|
} catch (RealmUnavailableException | SecurityException e) {
|
||||||
|
LOG.warn("Failed to authenticate user " + e.getMessage(), e);
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Tuple2<String, String> extractUsernamePassword(HttpServletRequest httpServletRequest) {
|
||||||
|
Optional<String[]> userPassOptional = extractAuthHeader(httpServletRequest);
|
||||||
|
|
||||||
|
if (userPassOptional.isPresent() && userPassOptional.get().length >= USERPASS_LENGTH) {
|
||||||
|
String[] userpass = userPassOptional.get();
|
||||||
|
String username = userpass[0];
|
||||||
|
String password = userpass.length > USERPASS_LENGTH ? userpass[1] : "";
|
||||||
|
|
||||||
|
return Tuple.of(username, password);
|
||||||
|
}
|
||||||
|
return Tuple.of("", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String[]> extractAuthHeader(HttpServletRequest httpServletRequest) {
|
||||||
|
Optional<String[]> retVal = Optional.empty();
|
||||||
|
|
||||||
|
String authorization = httpServletRequest.getHeader("Authorization");
|
||||||
|
if (authorization != null && StringUtils.length(authorization) > BASIC_HEADER_SIZE) {
|
||||||
|
authorization = authorization.substring(BASIC_HEADER_SIZE);
|
||||||
|
String decoded = StringUtils.toEncodedString(Base64.decodeBase64(authorization), Charset.forName("utf-8"));
|
||||||
|
retVal = Optional.of(decoded.split(":"));
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract device information from User-Agent header
|
||||||
|
*
|
||||||
|
* @param request HTTP servlet request
|
||||||
|
* @return Device/browser information
|
||||||
|
*/
|
||||||
|
public String extractDeviceInfo(HttpServletRequest request) {
|
||||||
|
String userAgent = request.getHeader("User-Agent");
|
||||||
|
return userAgent != null ? userAgent : "Unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract client IP address, considering proxies
|
||||||
|
*
|
||||||
|
* @param request HTTP servlet request
|
||||||
|
* @return Client IP address
|
||||||
|
*/
|
||||||
|
public String extractIpAddress(HttpServletRequest request) {
|
||||||
|
String xForwardedFor = request.getHeader("X-Forwarded-For");
|
||||||
|
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
|
||||||
|
// X-Forwarded-For can contain multiple IPs, take the first one
|
||||||
|
return xForwardedFor.split(",")[0].trim();
|
||||||
|
}
|
||||||
|
return request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,27 @@
|
|||||||
package marketing.heyday.hartmann.fotodocumentation.rest;
|
package marketing.heyday.hartmann.fotodocumentation.rest;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
import org.jboss.resteasy.annotations.GZIP;
|
import org.jboss.resteasy.annotations.GZIP;
|
||||||
|
import org.wildfly.security.auth.server.SecurityIdentity;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import jakarta.ejb.EJB;
|
import jakarta.ejb.EJB;
|
||||||
import jakarta.enterprise.context.RequestScoped;
|
import jakarta.enterprise.context.RequestScoped;
|
||||||
|
import jakarta.inject.Inject;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
import jakarta.ws.rs.POST;
|
import jakarta.ws.rs.POST;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
|
import jakarta.ws.rs.core.Context;
|
||||||
import jakarta.ws.rs.core.MediaType;
|
import jakarta.ws.rs.core.MediaType;
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import jakarta.ws.rs.core.Response.Status;
|
import jakarta.ws.rs.core.Response.Status;
|
||||||
import marketing.heyday.hartmann.fotodocumentation.core.service.CustomerPictureService;
|
import marketing.heyday.hartmann.fotodocumentation.core.service.CustomerPictureService;
|
||||||
|
import marketing.heyday.hartmann.fotodocumentation.core.utils.LoginUtils;
|
||||||
import marketing.heyday.hartmann.fotodocumentation.rest.jackson.JsonSchemaValidate;
|
import marketing.heyday.hartmann.fotodocumentation.rest.jackson.JsonSchemaValidate;
|
||||||
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerPictureValue;
|
import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerPictureValue;
|
||||||
|
|
||||||
@@ -28,18 +37,28 @@ import marketing.heyday.hartmann.fotodocumentation.rest.vo.CustomerPictureValue;
|
|||||||
@RequestScoped
|
@RequestScoped
|
||||||
@Path("customer-picture")
|
@Path("customer-picture")
|
||||||
public class CustomerPictureResource {
|
public class CustomerPictureResource {
|
||||||
|
private static final Log LOG = LogFactory.getLog(CustomerPictureResource.class);
|
||||||
|
|
||||||
@EJB
|
@EJB
|
||||||
private CustomerPictureService customerPictureService;
|
private CustomerPictureService customerPictureService;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
private LoginUtils loginUtils;
|
||||||
|
|
||||||
@GZIP
|
@GZIP
|
||||||
@POST
|
@POST
|
||||||
@Path("")
|
@Path("")
|
||||||
//@Authenticate(shouldBeInAllRoles = RightUtils.ADMIN_RIGHT)
|
|
||||||
@Consumes(MediaType.APPLICATION_JSON)
|
@Consumes(MediaType.APPLICATION_JSON)
|
||||||
@Operation(summary = "Add Customer Image to database")
|
@Operation(summary = "Add Customer Image to database")
|
||||||
@ApiResponse(responseCode = "200", description = "Add successfull")
|
@ApiResponse(responseCode = "200", description = "Add successfull")
|
||||||
public Response doAddCustomerPicture(@JsonSchemaValidate("schema/customer_picture_add.json") CustomerPictureValue customerPictureValue) {
|
public Response doAddCustomerPicture(@Context HttpServletRequest httpServletRequest, @JsonSchemaValidate("schema/customer_picture_add.json") CustomerPictureValue customerPictureValue) {
|
||||||
|
Optional<SecurityIdentity> identity = loginUtils.authenticate(httpServletRequest);
|
||||||
|
if (identity.isEmpty()) {
|
||||||
|
LOG.debug("identity empty login invalid");
|
||||||
|
return Response.status(401).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
boolean success = customerPictureService.addCustomerPicture(customerPictureValue);
|
boolean success = customerPictureService.addCustomerPicture(customerPictureValue);
|
||||||
|
|
||||||
return success ? Response.ok().build() : Response.status(Status.BAD_REQUEST).build();
|
return success ? Response.ok().build() : Response.status(Status.BAD_REQUEST).build();
|
||||||
|
|||||||
Binary file not shown.
@@ -144,6 +144,7 @@
|
|||||||
<subsystem xmlns="urn:jboss:domain:core-management:1.0"/>
|
<subsystem xmlns="urn:jboss:domain:core-management:1.0"/>
|
||||||
<subsystem xmlns="urn:jboss:domain:datasources:7.1">
|
<subsystem xmlns="urn:jboss:domain:datasources:7.1">
|
||||||
<datasources>
|
<datasources>
|
||||||
|
<!-- Patrick -->
|
||||||
<datasource jndi-name="java:/jdbc/fotoDocumentationDS" pool-name="fotoDocumentationDS" enabled="true" use-java-context="true" use-ccm="false">
|
<datasource jndi-name="java:/jdbc/fotoDocumentationDS" pool-name="fotoDocumentationDS" enabled="true" use-java-context="true" use-ccm="false">
|
||||||
<connection-url>jdbc:postgresql://hartmann_postgres:5432/fotodocumentation</connection-url>
|
<connection-url>jdbc:postgresql://hartmann_postgres:5432/fotodocumentation</connection-url>
|
||||||
<driver>postgres</driver>
|
<driver>postgres</driver>
|
||||||
@@ -168,6 +169,7 @@
|
|||||||
<security user-name="sa" password="sa"/>
|
<security user-name="sa" password="sa"/>
|
||||||
</datasource>
|
</datasource>
|
||||||
<drivers>
|
<drivers>
|
||||||
|
<!-- Patrick -->
|
||||||
<driver name="postgres" module="org.postgresql.jdbc">
|
<driver name="postgres" module="org.postgresql.jdbc">
|
||||||
<xa-datasource-class>org.postgresql.xa.PGXADataSource</xa-datasource-class>
|
<xa-datasource-class>org.postgresql.xa.PGXADataSource</xa-datasource-class>
|
||||||
</driver>
|
</driver>
|
||||||
@@ -300,7 +302,7 @@
|
|||||||
|
|
||||||
<!-- patrick -->
|
<!-- patrick -->
|
||||||
<jdbc-realm name="fotoDocumentationRealm" >
|
<jdbc-realm name="fotoDocumentationRealm" >
|
||||||
<principal-query data-source="fotoDocumentationDS" sql="select password, salt, ri.code as Role from x_user u left join role_to_right rtr on rtr.role_id_fk = u.role_id_fk left join x_right ri on rtr.right_id_fk = ri.right_id where username = ?;">
|
<principal-query data-source="fotoDocumentationDS" sql="select password, salt, ri.code as Role from x_user u left join user_to_right rtr on rtr.user_id_fk = u.user_id left join x_right ri on rtr.right_id_fk = ri.right_id where username = ?;">
|
||||||
<salted-simple-digest-mapper algorithm="password-salt-digest-sha-256" password-index="1" salt-index="2" />
|
<salted-simple-digest-mapper algorithm="password-salt-digest-sha-256" password-index="1" salt-index="2" />
|
||||||
<attribute-mapping>
|
<attribute-mapping>
|
||||||
<attribute to="groups" index="3"/>
|
<attribute to="groups" index="3"/>
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import org.junit.jupiter.api.TestMethodOrder;
|
|||||||
* created: 14 Nov 2024
|
* created: 14 Nov 2024
|
||||||
*/
|
*/
|
||||||
@TestMethodOrder(OrderAnnotation.class)
|
@TestMethodOrder(OrderAnnotation.class)
|
||||||
public class CustomerPictureTest extends AbstractRestTest {
|
public class CustomerPictureResourceTest extends AbstractRestTest {
|
||||||
private static final Log LOG = LogFactory.getLog(CustomerPictureTest.class);
|
private static final Log LOG = LogFactory.getLog(CustomerPictureResourceTest.class);
|
||||||
private static final String PATH = "api/customer-picture";
|
private static final String PATH = "api/customer-picture";
|
||||||
private static final String BASE_UPLOAD = "src/test/resources/upload/";
|
private static final String BASE_UPLOAD = "src/test/resources/upload/";
|
||||||
|
|
||||||
@@ -50,4 +50,18 @@ public class CustomerPictureTest extends AbstractRestTest {
|
|||||||
int code = httpResponse.getStatusLine().getStatusCode();
|
int code = httpResponse.getStatusLine().getStatusCode();
|
||||||
assertEquals(200, code);
|
assertEquals(200, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Order(2)
|
||||||
|
public void doAddCustomerPictureNoAuth() throws IOException {
|
||||||
|
LOG.info("doAddCustomerPicture");
|
||||||
|
|
||||||
|
String path = deploymentURL + PATH;
|
||||||
|
Request request = Request.Post(path).addHeader("Accept", "application/json; charset=utf-8")
|
||||||
|
.bodyFile(new File(BASE_UPLOAD + "add.json"), ContentType.APPLICATION_JSON);
|
||||||
|
|
||||||
|
HttpResponse httpResponse = executeRequest(request);
|
||||||
|
int code = httpResponse.getStatusLine().getStatusCode();
|
||||||
|
assertEquals(401, code);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user