Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MARP-1548 Suspicious installation counts #257

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
quanpham-axonivy marked this conversation as resolved.
Show resolved Hide resolved
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();
quanpham-axonivy marked this conversation as resolved.
Show resolved Hide resolved
}

}
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
Loading