добавление аудита в таблицу history
This commit is contained in:
parent
11b8f6aa82
commit
3ccb43cb6f
|
|
@ -15,9 +15,10 @@ FROM eclipse-temurin:21-jre
|
|||
|
||||
LABEL org.opencontainers.image.title="LTI Provider"
|
||||
LABEL org.opencontainers.image.description="LTI провайдер для лабораторных по Docker и Kubernetes"
|
||||
LABEL org.opencontainers.image.url="TODO"
|
||||
LABEL org.opencontainers.image.source="TODO"
|
||||
LABEL org.opencontainers.image.documentation="#TODO"
|
||||
LABEL org.opencontainers.image.url="https://git.oa2.ru/dzyk/lti-provider"
|
||||
LABEL org.opencontainers.image.source="https://git.oa2.ru/dzyk/lti-provider"
|
||||
LABEL org.opencontainers.image.documentation="External Tool для LMS, для лабораторных работо по Docker и Kubernetes.
|
||||
Поддерживает протокол LTI 1.3. Протестирован с OpenOLAT"
|
||||
|
||||
WORKDIR /opt
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,145 @@
|
|||
@startuml LTI Provider
|
||||
|
||||
' Настройка для A4 портретной ориентации с ГОСТ отступами (3см слева)
|
||||
skinparam dpi 150
|
||||
skinparam defaultFontSize 10
|
||||
|
||||
skinparam class {
|
||||
BackgroundColor<<Controller>> LightBlue
|
||||
BackgroundColor<<Service>> LightGreen
|
||||
BackgroundColor<<Entity>> Wheat
|
||||
BackgroundColor<<Infrastructure>> LightSalmon
|
||||
FontSize 10
|
||||
}
|
||||
|
||||
skinparam packageStyle rectangle
|
||||
skinparam linetype polyline
|
||||
|
||||
' Слой Controllers - верх по центру
|
||||
package "Controller Layer" {
|
||||
class LaunchController <<Controller>> {
|
||||
+ login()
|
||||
+ redirect()
|
||||
}
|
||||
|
||||
class TaskController <<Controller>> {
|
||||
+ getTask()
|
||||
+ updateTask()
|
||||
+ submitTask()
|
||||
}
|
||||
}
|
||||
|
||||
' Левая колонка - Service/Repository/Infrastructure
|
||||
package "Service Layer" {
|
||||
class TaskServiceImpl <<Service>> {
|
||||
+ getTask(UUID): TaskData
|
||||
+ saveTask(RequestUpdateTask)
|
||||
+ checkTask(ResultRequest)
|
||||
}
|
||||
|
||||
class JwtService <<Service>> {
|
||||
+ getPayload(jwt): Payload
|
||||
+ getTokenPayload(jwt): IdTokenPayload
|
||||
}
|
||||
|
||||
class HistoryService <<Service>> {
|
||||
+ logEvent()
|
||||
+ logAction()
|
||||
}
|
||||
}
|
||||
|
||||
package "Repository Layer" {
|
||||
interface LMSContentRepository {
|
||||
+ getLMSContentByContentUuid()
|
||||
}
|
||||
|
||||
interface HistoryRepository {
|
||||
+ findByContentUuid()
|
||||
+ findByDeploymentId()
|
||||
}
|
||||
}
|
||||
|
||||
package "Infrastructure Layer" {
|
||||
class LMSService <<Infrastructure>> {
|
||||
+ ltiAuth()
|
||||
+ exchangeForAccessToken()
|
||||
}
|
||||
|
||||
class Runner <<Infrastructure>> {
|
||||
+ run(userId, script)
|
||||
+ check(userId, script)
|
||||
}
|
||||
}
|
||||
|
||||
' Правая колонка - Entity/Domain
|
||||
package "Entity Layer" {
|
||||
class LMSContent <<Entity>> {
|
||||
- id: long
|
||||
- contentUuid: UUID
|
||||
- deploymentId: UUID
|
||||
- task: Task
|
||||
}
|
||||
|
||||
class Task <<Entity>> {
|
||||
- id: long
|
||||
- name: String
|
||||
- description: String
|
||||
- initScript: String
|
||||
- verificationScript: String
|
||||
- deleteScript: String
|
||||
}
|
||||
|
||||
class History <<Entity>> {
|
||||
- id: long
|
||||
- contentUuid: UUID
|
||||
- message: String
|
||||
}
|
||||
}
|
||||
|
||||
package "Domain Models" {
|
||||
class TaskData {
|
||||
- name: String
|
||||
- description: String
|
||||
- initScript: String
|
||||
- verificationScript: String
|
||||
- deleteScript: String
|
||||
}
|
||||
}
|
||||
|
||||
' Вертикальное выравнивание левой колонки
|
||||
"Controller Layer" -[hidden]down- "Service Layer"
|
||||
"Service Layer" -[hidden]down- "Repository Layer"
|
||||
"Repository Layer" -[hidden]down- "Infrastructure Layer"
|
||||
|
||||
' Вертикальное выравнивание правой колонки
|
||||
"Entity Layer" -[hidden]down- "Domain Models"
|
||||
|
||||
' Горизонтальное расположение колонок
|
||||
"Service Layer" -[hidden]right- "Entity Layer"
|
||||
"Repository Layer" -[hidden]right- "Entity Layer"
|
||||
"Infrastructure Layer" -[hidden]right- "Domain Models"
|
||||
|
||||
' Связи Controllers -> Services
|
||||
LaunchController --> JwtService
|
||||
LaunchController --> LMSService
|
||||
LaunchController --> HistoryService
|
||||
TaskController --> TaskServiceImpl
|
||||
TaskController --> HistoryService
|
||||
|
||||
' Связи Services -> Infrastructure/Repository
|
||||
TaskServiceImpl --> LMSContentRepository
|
||||
TaskServiceImpl --> Runner
|
||||
TaskServiceImpl --> LMSService
|
||||
TaskServiceImpl --> HistoryService
|
||||
TaskServiceImpl ..> TaskData
|
||||
|
||||
Runner --> HistoryService
|
||||
|
||||
' Связи Repository -> Entity
|
||||
LMSContentRepository ..> LMSContent
|
||||
HistoryRepository ..> History
|
||||
|
||||
' Связи Entity
|
||||
LMSContent "*" --> "1" Task
|
||||
|
||||
@enduml
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
@startuml
|
||||
skinparam backgroundColor white
|
||||
skinparam componentStyle rectangle
|
||||
|
||||
scale 1024 width
|
||||
|
||||
' Определяем участников системы
|
||||
package "Learning Platform" {
|
||||
[openOLAT] --> [External Tool]
|
||||
}
|
||||
|
||||
package "CI/CD Pipeline" {
|
||||
[GitLab] --> [GitLab Runner]
|
||||
[GitLab Runner] --> [Harbor]
|
||||
}
|
||||
|
||||
package "Deployment" {
|
||||
[Kubernetes]
|
||||
[Docker]
|
||||
}
|
||||
|
||||
actor User
|
||||
|
||||
' Связи
|
||||
User --> [External Tool]
|
||||
User --> [GitLab]
|
||||
User --> [Kubernetes]
|
||||
User --> [Docker]
|
||||
|
||||
[External Tool] --> [GitLab] : init/delete script
|
||||
[External Tool] --> [Kubernetes] : init/delete script
|
||||
[Harbor] --> [Kubernetes] : Pull images
|
||||
|
||||
"Learning Platform" -[hidden]down- "CI/CD Pipeline"
|
||||
"Learning Platform" -[hidden]down- "Deployment"
|
||||
|
||||
|
||||
|
||||
@enduml
|
||||
|
|
@ -6,6 +6,7 @@ import org.springframework.stereotype.Controller;
|
|||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import ru.oa2.lti.application.infrastructure.lms.LMSService;
|
||||
import ru.oa2.lti.application.service.history.HistoryService;
|
||||
import ru.oa2.lti.application.service.jwt.JwtService;
|
||||
import ru.oa2.lti.domain.model.auth.LtiLogin;
|
||||
|
||||
|
|
@ -21,11 +22,14 @@ public class LaunchController {
|
|||
|
||||
private final JwtService jwtService;
|
||||
private final LMSService lmsService;
|
||||
private final HistoryService historyService;
|
||||
|
||||
public LaunchController(JwtService jwtService,
|
||||
LMSService lmsService) {
|
||||
LMSService lmsService,
|
||||
HistoryService historyService) {
|
||||
this.jwtService = jwtService;
|
||||
this.lmsService = lmsService;
|
||||
this.historyService = historyService;
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
|
|
@ -57,6 +61,14 @@ public class LaunchController {
|
|||
session.setAttribute("lti_user_id", ltiLogin.getClientId());
|
||||
session.setAttribute("payload", payload);
|
||||
|
||||
// Логирование события входа
|
||||
historyService.logAction(
|
||||
payload.getDeploymentId(),
|
||||
payload.getContextId(),
|
||||
"LTI_LOGIN",
|
||||
String.format("User login via LTI 1.3, clientId=%s", clientId)
|
||||
);
|
||||
|
||||
if (payload.getContextId() != null) {
|
||||
return lmsService.ltiAuth(ltiMessageHint, iss, loginHint);
|
||||
}
|
||||
|
|
@ -79,6 +91,17 @@ public class LaunchController {
|
|||
session.setAttribute("id_token", idToken);
|
||||
session.setAttribute("state", state);
|
||||
|
||||
// Декодируем id_token для получения информации о контексте
|
||||
var idTokenPayload = jwtService.getTokenPayload(idToken);
|
||||
|
||||
// Логирование перенаправления после авторизации
|
||||
historyService.logAction(
|
||||
idTokenPayload.getDeploymentId(),
|
||||
idTokenPayload.getContext() != null ? idTokenPayload.getContext().id() : null,
|
||||
"LTI_REDIRECT",
|
||||
"User redirected after OAuth authentication"
|
||||
);
|
||||
|
||||
return "redirect:/tool/lti/task";
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import ru.oa2.lti.application.service.history.HistoryService;
|
||||
import ru.oa2.lti.application.service.task.TaskService;
|
||||
import ru.oa2.lti.domain.model.ResultRequest;
|
||||
import ru.oa2.lti.domain.model.auth.LtiLogin;
|
||||
|
|
@ -33,9 +34,11 @@ POST /submit - отправка на автоматическую проверк
|
|||
public class TaskController {
|
||||
|
||||
private final TaskService taskService;
|
||||
private final HistoryService historyService;
|
||||
|
||||
public TaskController(TaskService taskService) {
|
||||
public TaskController(TaskService taskService, HistoryService historyService) {
|
||||
this.taskService = taskService;
|
||||
this.historyService = historyService;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
|
|
@ -49,12 +52,26 @@ public class TaskController {
|
|||
var data = taskService.getTask(payload.getContextId());
|
||||
|
||||
if (data == null) {
|
||||
historyService.logAction(
|
||||
payload.getDeploymentId(),
|
||||
payload.getContextId(),
|
||||
"TASK_ERROR",
|
||||
"Task not found"
|
||||
);
|
||||
return "redirect:/error";
|
||||
}
|
||||
|
||||
model.addAttribute("name", data.getName());
|
||||
model.addAttribute("description", data.getDescription());
|
||||
|
||||
// Логирование просмотра задачи
|
||||
historyService.logAction(
|
||||
payload.getDeploymentId(),
|
||||
payload.getContextId(),
|
||||
"TASK_VIEW",
|
||||
String.format("Task viewed: %s, isCoach=%s", data.getName(), payload.getCoach())
|
||||
);
|
||||
|
||||
if (Objects.requireNonNull(payload).getCoach()) {
|
||||
model.addAttribute("initScript", data.getInitScript());
|
||||
model.addAttribute("checkScript", data.getVerificationScript());
|
||||
|
|
@ -75,6 +92,14 @@ public class TaskController {
|
|||
var payload = (Payload) session.getAttribute("payload");
|
||||
|
||||
if (payload != null && payload.getCoach()) {
|
||||
// Логирование обновления задачи
|
||||
historyService.logAction(
|
||||
payload.getDeploymentId(),
|
||||
payload.getContextId(),
|
||||
"TASK_UPDATE",
|
||||
String.format("Task updated by coach: %s", data.getName())
|
||||
);
|
||||
|
||||
return ResponseEntity.accepted().body(
|
||||
taskService.saveTask(RequestUpdateTask.builder()
|
||||
.contextId(payload.getContextId())
|
||||
|
|
@ -82,6 +107,17 @@ public class TaskController {
|
|||
.build())
|
||||
);
|
||||
}
|
||||
|
||||
// Логирование попытки несанкционированного обновления
|
||||
if (payload != null) {
|
||||
historyService.logAction(
|
||||
payload.getDeploymentId(),
|
||||
payload.getContextId(),
|
||||
"TASK_UPDATE_DENIED",
|
||||
"Unauthorized task update attempt"
|
||||
);
|
||||
}
|
||||
|
||||
return ResponseEntity.status(HttpStatusCode.valueOf(403))
|
||||
.body(new ResultResponse("forbidden"));
|
||||
}
|
||||
|
|
@ -94,6 +130,17 @@ public class TaskController {
|
|||
|
||||
var ltiLogin = (LtiLogin) session.getAttribute("lti_login");
|
||||
var idToken = (String) session.getAttribute("id_token");
|
||||
var payload = (Payload) session.getAttribute("payload");
|
||||
|
||||
// Логирование отправки задачи на проверку
|
||||
if (payload != null) {
|
||||
historyService.logAction(
|
||||
payload.getDeploymentId(),
|
||||
payload.getContextId(),
|
||||
"TASK_SUBMIT",
|
||||
String.format("Task submitted for checking, clientId=%s", ltiLogin.getClientId())
|
||||
);
|
||||
}
|
||||
|
||||
return ResponseEntity
|
||||
.accepted()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,16 @@
|
|||
package ru.oa2.lti.application.infrastructure.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import ru.oa2.lti.application.infrastructure.repository.entities.History;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface HistoryRepository extends JpaRepository<History, Long> {
|
||||
|
||||
List<History> findByContentUuidOrderByCreatedDesc(UUID contentUuid);
|
||||
|
||||
List<History> findByDeploymentIdOrderByCreatedDesc(UUID deploymentId);
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ package ru.oa2.lti.application.infrastructure.runner;
|
|||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import ru.oa2.lti.application.service.history.HistoryService;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
|
|
@ -17,13 +18,28 @@ import java.util.UUID;
|
|||
public class RunnerImpl implements Runner {
|
||||
|
||||
private final String TEMP_DIR = "./temp/";
|
||||
private final HistoryService historyService;
|
||||
|
||||
public RunnerImpl(HistoryService historyService) {
|
||||
this.historyService = historyService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean run(UUID userId, String script) {
|
||||
try {
|
||||
return runScript(userId, script);
|
||||
historyService.logAction(null, userId, "SCRIPT_RUN_START", "Init script execution started");
|
||||
boolean result = runScript(userId, script);
|
||||
|
||||
if (result) {
|
||||
historyService.logAction(null, userId, "SCRIPT_RUN_SUCCESS", "Init script completed successfully");
|
||||
} else {
|
||||
historyService.logAction(null, userId, "SCRIPT_RUN_FAILED", "Init script failed with non-zero exit code");
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
log.error(ex.getMessage());
|
||||
historyService.logAction(null, userId, "SCRIPT_RUN_ERROR", "Init script error: " + ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -32,9 +48,19 @@ public class RunnerImpl implements Runner {
|
|||
@Override
|
||||
public boolean check(UUID userId, String script) {
|
||||
try {
|
||||
return runScript(userId, script);
|
||||
historyService.logAction(null, userId, "SCRIPT_CHECK_START", "Verification script execution started");
|
||||
boolean result = runScript(userId, script);
|
||||
|
||||
if (result) {
|
||||
historyService.logAction(null, userId, "SCRIPT_CHECK_SUCCESS", "Verification script completed successfully");
|
||||
} else {
|
||||
historyService.logAction(null, userId, "SCRIPT_CHECK_FAILED", "Verification script failed");
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (Exception ex) {
|
||||
log.error(ex.getMessage());
|
||||
historyService.logAction(null, userId, "SCRIPT_CHECK_ERROR", "Verification script error: " + ex.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +68,7 @@ public class RunnerImpl implements Runner {
|
|||
@Override
|
||||
public boolean delete(UUID userId, String script) {
|
||||
//TODO реализовать метод удаления сущностей
|
||||
historyService.logAction(null, userId, "SCRIPT_DELETE_CALLED", "Delete script called (not implemented)");
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package ru.oa2.lti.application.service.history;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public interface HistoryService {
|
||||
|
||||
/**
|
||||
* Логирование события
|
||||
*
|
||||
* @param deploymentId ID деплоймента LMS
|
||||
* @param contentUuid UUID контента
|
||||
* @param message Сообщение о событии
|
||||
*/
|
||||
void logEvent(UUID deploymentId, UUID contentUuid, String message);
|
||||
|
||||
/**
|
||||
* Логирование события с автоматической меткой времени
|
||||
*
|
||||
* @param deploymentId ID деплоймента LMS
|
||||
* @param contentUuid UUID контента
|
||||
* @param action Действие (например, "LOGIN", "TASK_LOADED")
|
||||
* @param details Детали события
|
||||
*/
|
||||
void logAction(UUID deploymentId, UUID contentUuid, String action, String details);
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package ru.oa2.lti.application.service.history;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import ru.oa2.lti.application.infrastructure.repository.HistoryRepository;
|
||||
import ru.oa2.lti.application.infrastructure.repository.entities.History;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class HistoryServiceImpl implements HistoryService {
|
||||
|
||||
private final HistoryRepository historyRepository;
|
||||
|
||||
public HistoryServiceImpl(HistoryRepository historyRepository) {
|
||||
this.historyRepository = historyRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void logEvent(UUID deploymentId, UUID contentUuid, String message) {
|
||||
try {
|
||||
History history = new History();
|
||||
history.setDeploymentId(deploymentId);
|
||||
history.setContentUuid(contentUuid);
|
||||
history.setCreated(LocalDateTime.now());
|
||||
history.setMessage(truncateMessage(message));
|
||||
|
||||
historyRepository.save(history);
|
||||
log.debug("Event logged: deploymentId={}, contentUuid={}, message={}",
|
||||
deploymentId, contentUuid, message);
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to log event: deploymentId={}, contentUuid={}, message={}",
|
||||
deploymentId, contentUuid, message, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||
public void logAction(UUID deploymentId, UUID contentUuid, String action, String details) {
|
||||
String message = String.format("[%s] %s", action, details);
|
||||
logEvent(deploymentId, contentUuid, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрезка сообщения до максимальной длины (2048 символов)
|
||||
*/
|
||||
private String truncateMessage(String message) {
|
||||
if (message == null) {
|
||||
return "";
|
||||
}
|
||||
if (message.length() > 2048) {
|
||||
return message.substring(0, 2045) + "...";
|
||||
}
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
|
|||
import org.springframework.stereotype.Service;
|
||||
import ru.oa2.lti.application.infrastructure.lms.LMSService;
|
||||
import ru.oa2.lti.application.infrastructure.repository.LMSContentRepository;
|
||||
import ru.oa2.lti.application.service.history.HistoryService;
|
||||
import ru.oa2.lti.application.infrastructure.repository.entities.LMSContent;
|
||||
import ru.oa2.lti.application.infrastructure.repository.entities.Task;
|
||||
import ru.oa2.lti.application.infrastructure.runner.Runner;
|
||||
|
|
@ -23,6 +24,7 @@ public class TaskServiceImpl implements TaskService {
|
|||
|
||||
private final LMSContentRepository lmsContentRepository;
|
||||
private final Runner runner;
|
||||
private final HistoryService historyService;
|
||||
|
||||
//TODO объединить в один?
|
||||
final ResultService resultService;
|
||||
|
|
@ -31,12 +33,14 @@ public class TaskServiceImpl implements TaskService {
|
|||
public TaskServiceImpl(LMSContentRepository lmsContentRepository,
|
||||
Runner runner,
|
||||
ResultService resultService,
|
||||
LMSService lmsService) {
|
||||
LMSService lmsService,
|
||||
HistoryService historyService) {
|
||||
|
||||
this.lmsContentRepository = lmsContentRepository;
|
||||
this.runner = runner;
|
||||
this.resultService = resultService;
|
||||
this.lmsService = lmsService;
|
||||
this.historyService = historyService;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -88,9 +92,32 @@ public class TaskServiceImpl implements TaskService {
|
|||
Task task = content.getTask();
|
||||
|
||||
if (task != null) {
|
||||
String oldName = task.getName();
|
||||
updateTaskFromData(task, requestUpdateTask.getData());
|
||||
lmsContentRepository.save(content);
|
||||
|
||||
historyService.logAction(
|
||||
content.getDeploymentId(),
|
||||
requestUpdateTask.getContextId(),
|
||||
"TASK_SAVED",
|
||||
String.format("Task saved: %s (was: %s)",
|
||||
requestUpdateTask.getData().getName(), oldName)
|
||||
);
|
||||
} else {
|
||||
historyService.logAction(
|
||||
content.getDeploymentId(),
|
||||
requestUpdateTask.getContextId(),
|
||||
"TASK_SAVE_ERROR",
|
||||
"Task is null, cannot save"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
historyService.logAction(
|
||||
null,
|
||||
requestUpdateTask.getContextId(),
|
||||
"TASK_SAVE_ERROR",
|
||||
"Content not found"
|
||||
);
|
||||
}
|
||||
|
||||
//TODO обработка exception - failed / success
|
||||
|
|
|
|||
Loading…
Reference in New Issue