Skip to content

Commit

Permalink
MARP-1548 Suspicious installation counts (#257)
Browse files Browse the repository at this point in the history
  • Loading branch information
quanpham-axonivy authored Dec 17, 2024
1 parent 6ec5f3f commit e1a0d85
Show file tree
Hide file tree
Showing 19 changed files with 463 additions and 15 deletions.
3 changes: 2 additions & 1 deletion marketplace-build/.env
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ MARKET_GITHUB_OAUTH_APP_CLIENT_ID=
MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=
MARKET_JWT_SECRET_KEY=
MARKET_CORS_ALLOWED_ORIGIN=*
MARKET_MONGO_LOG_LEVEL=DEBUG
MARKET_MONGO_LOG_LEVEL=DEBUG
MARKET_LOG_PATH=logs
3 changes: 2 additions & 1 deletion marketplace-build/dev/.env
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ MARKET_GITHUB_OAUTH_APP_CLIENT_ID=
MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=
MARKET_JWT_SECRET_KEY=
MARKET_CORS_ALLOWED_ORIGIN=*
MARKET_MONGO_LOG_LEVEL=DEBUG
MARKET_MONGO_LOG_LEVEL=DEBUG
MARKET_LOG_PATH=logs
2 changes: 2 additions & 0 deletions marketplace-build/dev/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ services:
volumes:
- /home/axonivy/marketplace/data/market-installations.json:/app/data/market-installation.json
- marketcache:/app/data/market-cache
- ./logs:/app/logs
environment:
- MONGODB_HOST=${SERVICE_MONGODB_HOST}
- MONGODB_DATABASE=${SERVICE_MONGODB_DATABASE}
Expand All @@ -36,6 +37,7 @@ services:
- MARKET_JWT_SECRET_KEY=${MARKET_JWT_SECRET_KEY}
- MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN}
- MARKET_MONGO_LOG_LEVEL=${MARKET_MONGO_LOG_LEVEL}
- MARKET_LOG_PATH=${MARKET_LOG_PATH}
build:
context: ../../marketplace-service
dockerfile: Dockerfile
Expand Down
2 changes: 2 additions & 0 deletions marketplace-build/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ services:
volumes:
- /home/axonivy/marketplace/data/market-installations.json:/app/data/market-installation.json
- marketcache:/app/data/market-cache
- ./logs:/app/logs
environment:
- MONGODB_HOST=${SERVICE_MONGODB_HOST}
- MONGODB_DATABASE=${SERVICE_MONGODB_DATABASE}
Expand All @@ -36,6 +37,7 @@ services:
- MARKET_JWT_SECRET_KEY=${MARKET_JWT_SECRET_KEY}
- MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN}
- MARKET_MONGO_LOG_LEVEL=${MARKET_MONGO_LOG_LEVEL}
- MARKET_LOG_PATH=${MARKET_LOG_PATH}
build:
context: ../marketplace-service
dockerfile: Dockerfile
Expand Down
3 changes: 2 additions & 1 deletion marketplace-build/release/.env
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,5 @@ MARKET_GITHUB_OAUTH_APP_CLIENT_ID=
MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET=
MARKET_JWT_SECRET_KEY=
MARKET_CORS_ALLOWED_ORIGIN=*
MARKET_MONGO_LOG_LEVEL=DEBUG
MARKET_MONGO_LOG_LEVEL=DEBUG
MARKET_LOG_PATH=logs
2 changes: 2 additions & 0 deletions marketplace-build/release/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ services:
volumes:
- /home/axonivy/marketplace/data/market-installations.json:/app/data/market-installation.json
- marketcache:/app/data/market-cache
- ./logs:/app/logs
environment:
- MONGODB_HOST=${SERVICE_MONGODB_HOST}
- MONGODB_DATABASE=${SERVICE_MONGODB_DATABASE}
Expand All @@ -34,6 +35,7 @@ services:
- MARKET_JWT_SECRET_KEY=${MARKET_JWT_SECRET_KEY}
- MARKET_CORS_ALLOWED_ORIGIN=${MARKET_CORS_ALLOWED_ORIGIN}
- MARKET_MONGO_LOG_LEVEL=${MARKET_MONGO_LOG_LEVEL}
- MARKET_LOG_PATH=${MARKET_LOG_PATH}
networks:
- marketplace-network

Expand Down
6 changes: 5 additions & 1 deletion marketplace-service/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,11 @@
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class CommonConstants {
public static final String REQUESTED_BY = "X-Requested-By";
public static final String USER_AGENT = "user-agent";
public static final String SLASH = "/";
public static final String DOT_SEPARATOR = ".";
public static final String PLUS = "+";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.axonivy.market.constants;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LoggingConstants {

public static final String ENTRY_FORMAT = " <%s>%s</%s>%n";
public static final String ENTRY_START = " <LogEntry>\n";
public static final String ENTRY_END = " </LogEntry>\n";
public static final String DATE_FORMAT = "yyyy-MM-dd";
public static final String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String LOG_START = "<Logs>\n";
public static final String LOG_END = "</Logs>";
public static final String METHOD = "method";
public static final String ARGUMENTS = "arguments";
public static final String TIMESTAMP = "timestamp";
public static final String NO_ARGUMENTS = "No arguments";
public static final String MARKET_WEBSITE = "marketplace-website";

}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
import com.axonivy.market.constants.GitHubConstants;
import com.axonivy.market.enums.ErrorCode;
import com.axonivy.market.github.service.GitHubService;
import com.axonivy.market.logging.Loggable;
import com.axonivy.market.model.Message;
import com.axonivy.market.model.ProductCustomSortRequest;
import com.axonivy.market.service.ProductMarketplaceDataService;
import com.axonivy.market.util.AuthorizationUtils;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import lombok.AllArgsConstructor;
Expand All @@ -36,7 +35,7 @@
public class ProductMarketplaceDataController {
private final GitHubService gitHubService;
private final ProductMarketplaceDataService productMarketplaceDataService;

@PostMapping(CUSTOM_SORT)
@Operation(hidden = true)
public ResponseEntity<Message> createCustomSortProducts(
Expand All @@ -51,15 +50,14 @@ public ResponseEntity<Message> createCustomSortProducts(
return new ResponseEntity<>(message, HttpStatus.OK);
}

@Loggable
@Operation(hidden = true)
@PutMapping(INSTALLATION_COUNT_BY_ID)
@Operation(summary = "Update installation count of product",
description = "By default, increase installation count when click download product files by users")
public ResponseEntity<Integer> syncInstallationCount(
@PathVariable(ID) @Parameter(description = "Product id (from meta.json)", example = "approval-decision-utils",
in = ParameterIn.PATH) String productId,
@RequestParam(name = DESIGNER_VERSION, required = false) @Parameter(in = ParameterIn.QUERY,
example = "v10.0.20") String designerVersion) {
@PathVariable(ID) String productId,
@RequestParam(name = DESIGNER_VERSION, required = false) String designerVersion) {
int result = productMarketplaceDataService.updateInstallationCountForProduct(productId, designerVersion);
return new ResponseEntity<>(result, HttpStatus.OK);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.axonivy.market.logging;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.axonivy.market.logging;

import com.axonivy.market.constants.CommonConstants;
import com.axonivy.market.constants.LoggingConstants;
import com.axonivy.market.exceptions.model.MissingHeaderException;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;

import static com.axonivy.market.util.FileUtils.createFile;
import static com.axonivy.market.util.FileUtils.writeToFile;
import static com.axonivy.market.util.LoggingUtils.*;

@Log4j2
@Aspect
@Component
public class LoggableAspect {

@Value("${loggable.log-path}")
public String logFilePath;

@Before("@annotation(com.axonivy.market.logging.Loggable)")
public void logMethodCall(JoinPoint joinPoint) throws MissingHeaderException {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
ServletRequestAttributes attributes =
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
Map<String, String> headersMap = extractHeaders(request, signature, joinPoint);
saveLogToDailyFile(headersMap);

// block execution if request isn't from Market or Ivy Designer
if (!LoggingConstants.MARKET_WEBSITE.equals(headersMap.get(CommonConstants.REQUESTED_BY))) {
throw new MissingHeaderException();
}
}
}

private Map<String, String> extractHeaders(HttpServletRequest request, MethodSignature signature,
JoinPoint joinPoint) {
return Map.of(
LoggingConstants.METHOD, escapeXml(String.valueOf(signature.getMethod())),
LoggingConstants.TIMESTAMP, escapeXml(getCurrentTimestamp()),
CommonConstants.USER_AGENT, escapeXml(request.getHeader(CommonConstants.USER_AGENT)),
LoggingConstants.ARGUMENTS, escapeXml(getArgumentsString(signature.getParameterNames(), joinPoint.getArgs())),
CommonConstants.REQUESTED_BY, escapeXml(request.getHeader(CommonConstants.REQUESTED_BY))
);
}

// Use synchronized to prevent race condition
private synchronized void saveLogToDailyFile(Map<String, String> headersMap) {
try {
File logFile = createFile(generateFileName());

StringBuilder content = new StringBuilder();
if (logFile.exists()) {
content.append(new String(Files.readAllBytes(logFile.toPath())));
}
if (content.isEmpty()) {
content.append(LoggingConstants.LOG_START);
}
int lastLogIndex = content.lastIndexOf(LoggingConstants.LOG_END);
if (lastLogIndex != -1) {
content.delete(lastLogIndex, content.length());
}
content.append(buildLogEntry(headersMap));
content.append(LoggingConstants.LOG_END);

writeToFile(logFile, content.toString());
} catch (IOException e) {
log.error("Error writing log to file: {}", e.getMessage());
}
}

private String generateFileName() {
return Path.of(logFilePath, "log-" + getCurrentDate() + ".xml").toString();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.axonivy.market.util;

import lombok.AccessLevel;
import lombok.NoArgsConstructor;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class FileUtils {

public static File createFile(String fileName) throws IOException {
File file = new File(fileName);
File parentDir = file.getParentFile();
if (parentDir != null && !parentDir.exists() && !parentDir.mkdirs()) {
throw new IOException("Failed to create directory: " + parentDir.getAbsolutePath());
}
if (!file.exists() && !file.createNewFile()) {
throw new IOException("Failed to create file: " + file.getAbsolutePath());
}
return file;
}

public static void writeToFile(File file, String content) throws IOException {
try (FileWriter writer = new FileWriter(file, false)) {
writer.write(content);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.axonivy.market.util;

import com.axonivy.market.constants.LoggingConstants;
import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;

import java.text.SimpleDateFormat;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class LoggingUtils {

public static String getCurrentDate() {
return new SimpleDateFormat(LoggingConstants.DATE_FORMAT).format(System.currentTimeMillis());
}

public static String getCurrentTimestamp() {
return new SimpleDateFormat(LoggingConstants.TIMESTAMP_FORMAT).format(System.currentTimeMillis());
}

public static String escapeXml(String value) {
if (StringUtils.isEmpty(value)) {
return StringUtils.EMPTY;
}
return value.replace("&", "&amp;")
.replace("<", "&lt;")
.replace(">", "&gt;")
.replace("\"", "&quot;")
.replace("'", "&apos;");
}

public static String getArgumentsString(String[] paramNames, Object[] args) {
if (paramNames == null || paramNames.length == 0 || args == null || args.length == 0) {
return LoggingConstants.NO_ARGUMENTS;
}
return IntStream.range(0, paramNames.length)
.mapToObj(i -> paramNames[i] + ": " + args[i])
.collect(Collectors.joining(", "));
}

public static String buildLogEntry(Map<String, String> headersMap) {
StringBuilder logEntry = new StringBuilder();
Map<String, String> map = new TreeMap<>(headersMap);
logEntry.append(LoggingConstants.ENTRY_START);
map.forEach((key, value) -> logEntry.append(String.format(LoggingConstants.ENTRY_FORMAT, key, value, key)));
logEntry.append(LoggingConstants.ENTRY_END);
return logEntry.toString();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ market.github.oauth2-clientSecret=${MARKET_GITHUB_OAUTH_APP_CLIENT_SECRET}
jwt.secret=${MARKET_JWT_SECRET_KEY}
jwt.expiration=365
logging.level.org.springframework.data.mongodb.core.MongoTemplate=${MARKET_MONGO_LOG_LEVEL}
spring.jackson.serialization.indent_output=true
spring.jackson.serialization.indent_output=true
loggable.log-path=${MARKET_LOG_PATH}
Loading

0 comments on commit e1a0d85

Please sign in to comment.