union-ark-web 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/PROJECT_OVERVIEW.md +405 -0
  2. package/README.md +177 -0
  3. package/ark-control/pom.xml +142 -0
  4. package/ark-control/src/local-mock/java/com/epcc/commons/securityproxy/api/SecurityResult.java +14 -0
  5. package/ark-control/src/local-mock/java/com/epcc/commons/securityproxy/api/SymmetricalSecurityService.java +11 -0
  6. package/ark-control/src/local-mock/java/com/epcc/dubbo/result/Result.java +29 -0
  7. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/annotation/EnDecryptField.java +16 -0
  8. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/annotation/EnDecryptFieldLong.java +17 -0
  9. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/annotation/EnDecryptFieldWithTag.java +16 -0
  10. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/exception/BaseDataErrorCode.java +26 -0
  11. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/exception/CheckException.java +33 -0
  12. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/exception/ErrorCode.java +9 -0
  13. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/redis/RedisCacheService.java +131 -0
  14. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/util/Constant.java +24 -0
  15. package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/util/ResultUtil.java +9 -0
  16. package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalMockSensitiveProxy.java +40 -0
  17. package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalRedisCacheService.java +45 -0
  18. package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalSensitiveHostConfiguration.java +27 -0
  19. package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalSensitiveProxyConfiguration.java +16 -0
  20. package/ark-control/src/main/java/com/union/control/ArkControlApplication.java +17 -0
  21. package/ark-control/src/main/java/com/union/control/dubbo/ProviderAccessLogFilter.java +45 -0
  22. package/ark-control/src/main/java/com/union/control/mapper/AgentExecutionMapper.java +35 -0
  23. package/ark-control/src/main/java/com/union/control/mapper/ConversationMapper.java +44 -0
  24. package/ark-control/src/main/java/com/union/control/mapper/MemoryStoreMapper.java +41 -0
  25. package/ark-control/src/main/java/com/union/control/mapper/ScheduledTaskMapper.java +96 -0
  26. package/ark-control/src/main/java/com/union/control/mapper/SensitiveAddressBookDemo.java +31 -0
  27. package/ark-control/src/main/java/com/union/control/mapper/SensitiveDataDemoMapper.java +24 -0
  28. package/ark-control/src/main/java/com/union/control/mapper/interceptor/AESInterceptor.java +265 -0
  29. package/ark-control/src/main/java/com/union/control/mapper/interceptor/AddressBookHandler.java +105 -0
  30. package/ark-control/src/main/java/com/union/control/schedule/ScheduledExecutionToken.java +55 -0
  31. package/ark-control/src/main/java/com/union/control/schedule/ScheduledTaskScheduler.java +206 -0
  32. package/ark-control/src/main/java/com/union/control/service/impl/AgentExecutionServiceImpl.java +212 -0
  33. package/ark-control/src/main/java/com/union/control/service/impl/AgentProxyServiceImpl.java +143 -0
  34. package/ark-control/src/main/java/com/union/control/service/impl/ConversationServiceImpl.java +245 -0
  35. package/ark-control/src/main/java/com/union/control/service/impl/MemoryStoreServiceImpl.java +252 -0
  36. package/ark-control/src/main/java/com/union/control/service/impl/RunningAnalysisMockServiceImpl.java +301 -0
  37. package/ark-control/src/main/java/com/union/control/service/impl/ScheduledTaskServiceImpl.java +576 -0
  38. package/ark-control/src/main/java/com/union/control/service/impl/SensitiveDataDemoServiceImpl.java +144 -0
  39. package/ark-control/src/main/java/com/union/control/service/sensitive/RedisRevealTokenStore.java +90 -0
  40. package/ark-control/src/main/java/com/union/control/service/sensitive/SensitiveRevealProcessor.java +319 -0
  41. package/ark-control/src/main/java/com/union/control/service/sensitive/SensitiveRevealServiceImpl.java +72 -0
  42. package/ark-control/src/main/java/com/union/control/utils/AgentSupport.java +103 -0
  43. package/ark-control/src/main/java/com/union/control/utils/security/SymmetricalSecurityUtils.java +160 -0
  44. package/ark-control/src/main/resources/META-INF/dubbo/com.alibaba.dubbo.rpc.Filter +1 -0
  45. package/ark-control/src/main/resources/application.properties +4 -0
  46. package/ark-control/src/main/resources/application.yml +32 -0
  47. package/ark-control/src/main/resources/dubbo-provider.xml +20 -0
  48. package/ark-control/src/main/resources/mapper/AgentExecutionMapper.xml +75 -0
  49. package/ark-control/src/main/resources/mapper/ConversationMapper.xml +110 -0
  50. package/ark-control/src/main/resources/mapper/MemoryStoreMapper.xml +74 -0
  51. package/ark-control/src/main/resources/mapper/ScheduledTaskMapper.xml +286 -0
  52. package/ark-control/src/main/resources/mapper/SensitiveDataDemoMapper.xml +55 -0
  53. package/ark-control/src/main/resources/schema.sql +146 -0
  54. package/ark-control/src/test/java/com/union/control/AgentExecutionServiceTest.java +170 -0
  55. package/ark-control/src/test/java/com/union/control/ArkControlStructureContractTest.java +107 -0
  56. package/ark-control/src/test/java/com/union/control/ConversationServiceTest.java +89 -0
  57. package/ark-control/src/test/java/com/union/control/MemoryStoreServiceTest.java +26 -0
  58. package/ark-control/src/test/java/com/union/control/PydanticAiControlContractTest.java +52 -0
  59. package/ark-control/src/test/java/com/union/control/TestJson.java +45 -0
  60. package/ark-control/src/test/java/com/union/control/dubbo/ProviderAccessLogFilterTest.java +53 -0
  61. package/ark-control/src/test/java/com/union/control/local/sensitive/LocalSensitiveProxyConfigurationTest.java +23 -0
  62. package/ark-control/src/test/java/com/union/control/mapper/interceptor/AESInterceptorCompatibilityTest.java +505 -0
  63. package/ark-control/src/test/java/com/union/control/schedule/ScheduledExecutionTokenTest.java +27 -0
  64. package/ark-control/src/test/java/com/union/control/schedule/ScheduledTaskSchedulerTest.java +51 -0
  65. package/ark-control/src/test/java/com/union/control/service/AgentProxyServiceTest.java +111 -0
  66. package/ark-control/src/test/java/com/union/control/service/ScheduledTaskServiceTest.java +483 -0
  67. package/ark-control/src/test/java/com/union/control/service/impl/SensitiveDataDemoServiceImplTest.java +104 -0
  68. package/ark-control/src/test/java/com/union/control/service/sensitive/RedisRevealTokenStoreTest.java +65 -0
  69. package/ark-control/src/test/java/com/union/control/service/sensitive/SensitiveRevealBoundaryTest.java +99 -0
  70. package/ark-control/src/test/java/com/union/control/service/sensitive/SensitiveRevealProcessorCodecTest.java +111 -0
  71. package/ark-control/src/test/java/com/union/control/service/sensitive/SensitiveRevealProcessorTest.java +111 -0
  72. package/ark-control/src/test/java/com/union/control/utils/security/SymmetricalSecurityUtilsCompatibilityTest.java +87 -0
  73. package/ark-control-facade/pom.xml +16 -0
  74. package/ark-control-facade/src/main/java/com/union/control/service/AgentExecutionService.java +8 -0
  75. package/ark-control-facade/src/main/java/com/union/control/service/AgentProxyService.java +7 -0
  76. package/ark-control-facade/src/main/java/com/union/control/service/AgentResponse.java +22 -0
  77. package/ark-control-facade/src/main/java/com/union/control/service/ConversationService.java +12 -0
  78. package/ark-control-facade/src/main/java/com/union/control/service/MemoryStoreService.java +12 -0
  79. package/ark-control-facade/src/main/java/com/union/control/service/RunningAnalysisMockService.java +10 -0
  80. package/ark-control-facade/src/main/java/com/union/control/service/ScheduledTaskService.java +40 -0
  81. package/ark-control-facade/src/main/java/com/union/control/service/SensitiveDataDemoService.java +13 -0
  82. package/ark-control-facade/src/main/java/com/union/control/service/sensitive/SensitiveRevealService.java +18 -0
  83. package/ark-web/README.md +80 -0
  84. package/ark-web/pom.xml +105 -0
  85. package/ark-web/src/local-mock/java/com/epcc/arkweb/mock/ArkAuthServiceMock.java +105 -0
  86. package/ark-web/src/local-mock/java/com/epcc/arkweb/mock/LocalKaptchaConfiguration.java +26 -0
  87. package/ark-web/src/main/java/com/epcc/arkweb/Application.java +13 -0
  88. package/ark-web/src/main/java/com/epcc/arkweb/config/InterceptorConfig.java +20 -0
  89. package/ark-web/src/main/java/com/epcc/arkweb/config/Realm.java +64 -0
  90. package/ark-web/src/main/java/com/epcc/arkweb/config/ShiroConfig.java +129 -0
  91. package/ark-web/src/main/java/com/epcc/arkweb/filter/LoginFormFilter.java +43 -0
  92. package/ark-web/src/main/java/com/epcc/arkweb/helper/AuthContextHolder.java +39 -0
  93. package/ark-web/src/main/java/com/epcc/arkweb/helper/AuthenticatedRequest.java +91 -0
  94. package/ark-web/src/main/java/com/epcc/arkweb/model/ShiroUser.java +21 -0
  95. package/ark-web/src/main/java/com/epcc/arkweb/utils/ResultMsg.java +19 -0
  96. package/ark-web/src/main/java/com/epcc/arkweb/vo/llm/ScheduledTaskCommandVO.java +30 -0
  97. package/ark-web/src/main/java/com/epcc/arkweb/vo/llm/ScheduledTaskQueryVO.java +20 -0
  98. package/ark-web/src/main/java/com/epcc/arkweb/web/CommonController.java +25 -0
  99. package/ark-web/src/main/java/com/epcc/arkweb/web/llm/AgentAuthorizationInterceptor.java +229 -0
  100. package/ark-web/src/main/java/com/epcc/arkweb/web/llm/AgentController.java +165 -0
  101. package/ark-web/src/main/java/com/epcc/arkweb/web/llm/AgentPermission.java +13 -0
  102. package/ark-web/src/main/java/com/epcc/arkweb/web/llm/LlmController.java +209 -0
  103. package/ark-web/src/main/java/com/epcc/arkweb/web/llm/ScheduledTaskController.java +160 -0
  104. package/ark-web/src/main/java/com/epcc/arkweb/web/sensitive/reveal/SensitiveCaptchaService.java +146 -0
  105. package/ark-web/src/main/java/com/epcc/arkweb/web/sensitive/reveal/SensitiveRevealController.java +163 -0
  106. package/ark-web/src/main/resources/application.properties +1 -0
  107. package/ark-web/src/main/resources/application.yml +23 -0
  108. package/ark-web/src/main/resources/dubbo-consumer.xml +19 -0
  109. package/ark-web/src/production-overlay/java/com/epcc/arkweb/config/CasShiroConfig.java +229 -0
  110. package/ark-web/src/production-overlay/java/com/epcc/arkweb/config/InterceptorConfig.java +35 -0
  111. package/ark-web/src/production-overlay/java/com/epcc/arkweb/config/ShiroConfig.java +161 -0
  112. package/ark-web/src/test/java/com/epcc/arkweb/ArkWebStructureContractTest.java +290 -0
  113. package/ark-web/src/test/java/com/epcc/arkweb/ShiroTestSupport.java +32 -0
  114. package/ark-web/src/test/java/com/epcc/arkweb/config/LocalShiroIntegrationTest.java +121 -0
  115. package/ark-web/src/test/java/com/epcc/arkweb/helper/AuthenticatedRequestTest.java +131 -0
  116. package/ark-web/src/test/java/com/epcc/arkweb/local/LocalArkAuthServiceMockTest.java +17 -0
  117. package/ark-web/src/test/java/com/epcc/arkweb/web/llm/AgentAuthorizationInterceptorTest.java +134 -0
  118. package/ark-web/src/test/java/com/epcc/arkweb/web/llm/AgentControllerAuthorizationTest.java +97 -0
  119. package/ark-web/src/test/java/com/epcc/arkweb/web/llm/LlmControllerTest.java +113 -0
  120. package/ark-web/src/test/java/com/epcc/arkweb/web/sensitive/reveal/SensitiveRevealControllerTest.java +273 -0
  121. package/package.json +18 -0
  122. package/pom.xml +16 -0
@@ -0,0 +1,134 @@
1
+ package com.epcc.arkweb.web.llm;
2
+
3
+ import com.union.control.service.ScheduledTaskService;
4
+ import org.junit.After;
5
+ import org.junit.Test;
6
+ import org.springframework.mock.web.MockHttpServletRequest;
7
+ import org.springframework.mock.web.MockHttpServletResponse;
8
+ import org.springframework.web.context.request.RequestContextHolder;
9
+ import org.springframework.web.context.request.ServletRequestAttributes;
10
+ import org.springframework.web.method.HandlerMethod;
11
+
12
+ import java.lang.reflect.Method;
13
+ import java.util.Collections;
14
+ import java.util.LinkedHashMap;
15
+ import java.util.Map;
16
+
17
+ import static org.assertj.core.api.Assertions.assertThat;
18
+ import static org.mockito.Mockito.mock;
19
+ import static org.mockito.Mockito.when;
20
+
21
+ public class AgentAuthorizationInterceptorTest {
22
+ private static final String TOKEN = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
23
+
24
+ @After
25
+ public void clearRequest() {
26
+ RequestContextHolder.resetRequestAttributes();
27
+ }
28
+
29
+ @Test
30
+ public void rawScheduledBearerCannotSkipTheAuthorizationEndpoint() throws Exception {
31
+ AgentAuthorizationInterceptor interceptor = interceptor(mock(ScheduledTaskService.class));
32
+ MockHttpServletRequest request = new MockHttpServletRequest("POST", "/agent/tool");
33
+ request.addHeader("Authorization", "Scheduled " + TOKEN);
34
+ MockHttpServletResponse response = new MockHttpServletResponse();
35
+
36
+ assertThat(interceptor.preHandle(request, response, handler())).isFalse();
37
+ assertThat(response.getStatus()).isEqualTo(401);
38
+ }
39
+
40
+ @Test
41
+ public void trustedContextUsesDatabaseIdentityAndLiveResources() throws Exception {
42
+ ScheduledTaskService tasks = mock(ScheduledTaskService.class);
43
+ when(tasks.resolveScheduledToken(TOKEN)).thenReturn(row());
44
+ AgentAuthorizationInterceptor interceptor = interceptor(tasks);
45
+ MockHttpServletRequest request = new MockHttpServletRequest("POST", "/agent/tool");
46
+ request.addHeader(AgentAuthorizationInterceptor.TRUSTED_CONTEXT_HEADER,
47
+ "Scheduled " + TOKEN);
48
+ MockHttpServletResponse response = new MockHttpServletResponse();
49
+ RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request));
50
+
51
+ assertThat(interceptor.preHandle(request, response, handler())).isTrue();
52
+ assertThat(AgentAuthorizationInterceptor.currentContext().get("userId"))
53
+ .isEqualTo("user-1");
54
+ assertThat(AgentAuthorizationInterceptor.currentContext().get("roleId"))
55
+ .isEqualTo("role-1");
56
+ }
57
+
58
+ @Test
59
+ public void missingProductionAuthServiceFailsClosed() throws Exception {
60
+ ScheduledTaskService tasks = mock(ScheduledTaskService.class);
61
+ when(tasks.resolveScheduledToken(TOKEN)).thenReturn(row());
62
+ AgentAuthorizationInterceptor interceptor = new AgentAuthorizationInterceptor(tasks);
63
+ MockHttpServletRequest request = new MockHttpServletRequest("POST", "/agent/tool");
64
+ request.addHeader(AgentAuthorizationInterceptor.TRUSTED_CONTEXT_HEADER,
65
+ "Scheduled " + TOKEN);
66
+ MockHttpServletResponse response = new MockHttpServletResponse();
67
+
68
+ assertThat(interceptor.preHandle(request, response, handler())).isFalse();
69
+ assertThat(response.getStatus()).isEqualTo(403);
70
+ }
71
+
72
+ @Test
73
+ public void unannotatedAgentMethodFailsClosed() throws Exception {
74
+ AgentAuthorizationInterceptor interceptor = interceptor(mock(ScheduledTaskService.class));
75
+ MockHttpServletRequest request = new MockHttpServletRequest("POST", "/agent/newTool");
76
+ MockHttpServletResponse response = new MockHttpServletResponse();
77
+ Method method = Probe.class.getMethod("unprotectedTool");
78
+
79
+ assertThat(interceptor.preHandle(
80
+ request, response, new HandlerMethod(new Probe(), method))).isFalse();
81
+ assertThat(response.getStatus()).isEqualTo(403);
82
+ }
83
+
84
+ private static AgentAuthorizationInterceptor interceptor(ScheduledTaskService tasks) {
85
+ AgentAuthorizationInterceptor interceptor = new AgentAuthorizationInterceptor(tasks);
86
+ interceptor.setArkAuthService(new FakeArkAuthService());
87
+ return interceptor;
88
+ }
89
+
90
+ private static HandlerMethod handler() throws Exception {
91
+ Method method = Probe.class.getMethod("tool");
92
+ return new HandlerMethod(new Probe(), method);
93
+ }
94
+
95
+ private static Map<String, Object> row() {
96
+ Map<String, Object> row = new LinkedHashMap<>();
97
+ row.put("runId", 7L);
98
+ row.put("taskId", 8L);
99
+ row.put("userId", "user-1");
100
+ row.put("orgCode", "org-1");
101
+ row.put("roleId", "role-1");
102
+ row.put("prompt", "prompt");
103
+ row.put("scheduledAt", "2026-09-02T01:00:00.000Z");
104
+ row.put("timezone", "UTC");
105
+ row.put("expiresAt", "2030-09-02T02:00:00.000Z");
106
+ return row;
107
+ }
108
+
109
+ public static final class Probe {
110
+ @AgentPermission("/assistantManager/page")
111
+ public void tool() {}
112
+ public void unprotectedTool() {}
113
+ }
114
+
115
+ public static final class FakeArkAuthService {
116
+ public FakeResult queryResource(String roleId, String userName, String traceNo) {
117
+ return new FakeResult(Collections.singletonList(
118
+ new FakeResource("/assistantManager/page")));
119
+ }
120
+ }
121
+
122
+ public static final class FakeResult {
123
+ private final Object result;
124
+ FakeResult(Object result) { this.result = result; }
125
+ public boolean isSuccess() { return true; }
126
+ public Object getResult() { return result; }
127
+ }
128
+
129
+ public static final class FakeResource {
130
+ private final String url;
131
+ FakeResource(String url) { this.url = url; }
132
+ public String getResourceUrl() { return url; }
133
+ }
134
+ }
@@ -0,0 +1,97 @@
1
+ package com.epcc.arkweb.web.llm;
2
+
3
+ import com.epcc.arkweb.helper.AuthenticatedRequest;
4
+ import com.union.control.service.AgentExecutionService;
5
+ import com.union.control.service.ConversationService;
6
+ import com.union.control.service.MemoryStoreService;
7
+ import com.union.control.service.RunningAnalysisMockService;
8
+ import com.union.control.service.ScheduledTaskService;
9
+ import org.junit.Test;
10
+ import org.springframework.http.HttpStatus;
11
+ import org.springframework.http.ResponseEntity;
12
+
13
+ import java.util.Collections;
14
+ import java.util.LinkedHashMap;
15
+ import java.util.Map;
16
+
17
+ import static org.assertj.core.api.Assertions.assertThat;
18
+ import static org.mockito.Mockito.mock;
19
+ import static org.mockito.Mockito.when;
20
+
21
+ public class AgentControllerAuthorizationTest {
22
+ @Test
23
+ public void returnsTrustedContextOnlyAfterControlAndResourceChecks() {
24
+ ScheduledTaskService tasks = mock(ScheduledTaskService.class);
25
+ String token = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
26
+ Map<String, Object> row = new LinkedHashMap<>();
27
+ row.put("runId", 7L);
28
+ row.put("taskId", 8L);
29
+ row.put("userId", "user-1");
30
+ row.put("orgCode", "org-1");
31
+ row.put("roleId", "role-1");
32
+ row.put("prompt", "prompt");
33
+ row.put("scheduledAt", "2026-09-02T01:00:00.000Z");
34
+ row.put("timezone", "UTC");
35
+ row.put("expiresAt", "2030-09-02T02:00:00.000Z");
36
+ when(tasks.resolveScheduledToken(token)).thenReturn(row);
37
+ AgentAuthorizationInterceptor authorization = new AgentAuthorizationInterceptor(tasks);
38
+ authorization.setArkAuthService(new FakeArkAuthService(true));
39
+ AgentController controller = new AgentController(
40
+ mock(ConversationService.class), mock(AgentExecutionService.class),
41
+ mock(MemoryStoreService.class), mock(RunningAnalysisMockService.class),
42
+ mock(AuthenticatedRequest.class), authorization);
43
+
44
+ ResponseEntity<Map<String, Object>> response = controller.scheduledTaskAuthorize(
45
+ "Scheduled " + token, Collections.emptyMap());
46
+
47
+ assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
48
+ assertThat(response.getBody().get("success")).isEqualTo(true);
49
+ @SuppressWarnings("unchecked") Map<String, Object> data =
50
+ (Map<String, Object>) response.getBody().get("data");
51
+ assertThat(data).containsEntry("userId", "user-1")
52
+ .containsEntry("roleId", "role-1")
53
+ .containsEntry("trustedContext", "Scheduled " + token);
54
+ }
55
+
56
+ @Test
57
+ public void rejectsIdentityFieldsInTheAuthorizationRequestBody() {
58
+ AgentAuthorizationInterceptor authorization = new AgentAuthorizationInterceptor(
59
+ mock(ScheduledTaskService.class));
60
+ authorization.setArkAuthService(new FakeArkAuthService(true));
61
+ AgentController controller = new AgentController(
62
+ mock(ConversationService.class), mock(AgentExecutionService.class),
63
+ mock(MemoryStoreService.class), mock(RunningAnalysisMockService.class),
64
+ mock(AuthenticatedRequest.class), authorization);
65
+ Map<String, Object> payload = new LinkedHashMap<>();
66
+ payload.put("roleId", "attacker-role");
67
+
68
+ assertThat(controller.scheduledTaskAuthorize(
69
+ "Scheduled AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", payload)
70
+ .getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED);
71
+ }
72
+
73
+ public static final class FakeArkAuthService {
74
+ private final boolean allowed;
75
+
76
+ FakeArkAuthService(boolean allowed) { this.allowed = allowed; }
77
+
78
+ public FakeResult queryResource(String roleId, String userName, String traceNo) {
79
+ return new FakeResult(allowed
80
+ ? Collections.singletonList(new FakeResource("/assistantManager/page"))
81
+ : Collections.<FakeResource>emptyList());
82
+ }
83
+ }
84
+
85
+ public static final class FakeResult {
86
+ private final Object result;
87
+ FakeResult(Object result) { this.result = result; }
88
+ public boolean isSuccess() { return true; }
89
+ public Object getResult() { return result; }
90
+ }
91
+
92
+ public static final class FakeResource {
93
+ private final String url;
94
+ FakeResource(String url) { this.url = url; }
95
+ public String getResourceUrl() { return url; }
96
+ }
97
+ }
@@ -0,0 +1,113 @@
1
+ package com.epcc.arkweb.web.llm;
2
+
3
+ import com.epcc.arkweb.helper.AuthenticatedRequest;
4
+ import com.union.control.service.AgentExecutionService;
5
+ import com.union.control.service.AgentResponse;
6
+ import com.union.control.service.AgentProxyService;
7
+ import com.union.control.service.ConversationService;
8
+ import org.junit.Test;
9
+ import org.springframework.http.HttpHeaders;
10
+ import org.springframework.http.HttpMethod;
11
+ import org.springframework.http.MediaType;
12
+ import org.springframework.http.ResponseEntity;
13
+ import org.springframework.mock.web.MockHttpServletResponse;
14
+ import org.springframework.test.web.client.MockRestServiceServer;
15
+ import org.springframework.web.client.RestTemplate;
16
+
17
+ import java.nio.charset.StandardCharsets;
18
+ import java.util.LinkedHashMap;
19
+ import java.util.Map;
20
+
21
+ import static org.assertj.core.api.Assertions.assertThat;
22
+ import static org.mockito.Mockito.mock;
23
+ import static org.mockito.Mockito.when;
24
+ import static org.springframework.test.web.client.match.MockRestRequestMatchers.header;
25
+ import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
26
+ import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
27
+ import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
28
+
29
+ public class LlmControllerTest {
30
+ @Test
31
+ public void returnsSynchronousAgentResponseAsJson() {
32
+ ConversationService conversations = mock(ConversationService.class);
33
+ AgentExecutionService executions = mock(AgentExecutionService.class);
34
+ AgentProxyService gateway = mock(AgentProxyService.class);
35
+ AuthenticatedRequest request = mock(AuthenticatedRequest.class);
36
+ byte[] payload = "{}".getBytes(StandardCharsets.UTF_8);
37
+ when(request.clientPayload(payload)).thenReturn(payload);
38
+ when(gateway.sync("CASSESSIONID=session", "{}"))
39
+ .thenReturn(new AgentResponse(200, "{\"content\":{\"status\":\"READY\"}}"));
40
+
41
+ ResponseEntity<String> response = new LlmController(
42
+ conversations, executions, gateway, request, "http://py-app", new RestTemplate())
43
+ .chatMessageSync("CASSESSIONID=session", payload);
44
+
45
+ assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
46
+ assertThat(response.getBody()).isEqualTo("{\"content\":{\"status\":\"READY\"}}");
47
+ }
48
+
49
+ @Test
50
+ public void streamsPyAppHttpResponseDirectlyToTheBrowser() throws Exception {
51
+ ConversationService conversations = mock(ConversationService.class);
52
+ AgentExecutionService executions = mock(AgentExecutionService.class);
53
+ AgentProxyService gateway = mock(AgentProxyService.class);
54
+ AuthenticatedRequest request = mock(AuthenticatedRequest.class);
55
+ RestTemplate http = new RestTemplate();
56
+ MockRestServiceServer server = MockRestServiceServer.createServer(http);
57
+ byte[] payload = "{}".getBytes(StandardCharsets.UTF_8);
58
+ when(request.clientPayload(payload)).thenReturn(payload);
59
+ when(request.json(payload)).thenReturn("claim");
60
+ Map<String, Object> execution = new LinkedHashMap<>();
61
+ execution.put("conversationId", "conversation-1");
62
+ execution.put("runId", "run-1");
63
+ Map<String, Object> claimed = new LinkedHashMap<>();
64
+ claimed.put("data", execution);
65
+ when(executions.claimAguiRun("claim")).thenReturn(claimed);
66
+ server.expect(requestTo("http://py-app/agent/v1/runs"))
67
+ .andExpect(method(HttpMethod.POST))
68
+ .andExpect(header(HttpHeaders.COOKIE, "CASSESSIONID=session"))
69
+ .andRespond(withSuccess("data: first\n\ndata: second\n\n", MediaType.TEXT_EVENT_STREAM));
70
+ MockHttpServletResponse response = new MockHttpServletResponse();
71
+
72
+ new LlmController(conversations, executions, gateway, request, "http://py-app", http)
73
+ .chatMessage("CASSESSIONID=session", payload, response);
74
+
75
+ assertThat(response.getContentAsString()).isEqualTo("data: first\n\ndata: second\n\n");
76
+ assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("text/event-stream");
77
+ assertThat(response.getHeader("X-Accel-Buffering")).isEqualTo("no");
78
+ server.verify();
79
+ }
80
+ @Test(timeout = 5000)
81
+ public void streamingStopsWaitingForASilentUpstream() throws Exception {
82
+ com.sun.net.httpserver.HttpServer server = com.sun.net.httpserver.HttpServer.create(
83
+ new java.net.InetSocketAddress("127.0.0.1", 0), 0);
84
+ server.createContext("/", exchange -> {
85
+ try { Thread.sleep(150); }
86
+ catch (InterruptedException error) { Thread.currentThread().interrupt(); }
87
+ finally { exchange.close(); }
88
+ });
89
+ server.start();
90
+ try {
91
+ AuthenticatedRequest request = mock(AuthenticatedRequest.class);
92
+ AgentExecutionService executions = mock(AgentExecutionService.class);
93
+ byte[] payload = "{}".getBytes(StandardCharsets.UTF_8);
94
+ when(request.clientPayload(payload)).thenReturn(payload);
95
+ when(request.json(payload)).thenReturn("claim");
96
+ Map<String, Object> execution = new LinkedHashMap<>();
97
+ execution.put("conversationId", "thread-1");
98
+ execution.put("runId", "run-1");
99
+ when(executions.claimAguiRun("claim"))
100
+ .thenReturn(java.util.Collections.singletonMap("data", execution));
101
+ LlmController controller = new LlmController(mock(ConversationService.class), executions,
102
+ mock(AgentProxyService.class), request,
103
+ "http://127.0.0.1:" + server.getAddress().getPort(), 0.05);
104
+ org.assertj.core.api.Assertions.assertThatThrownBy(() -> controller.chatMessage(
105
+ "session", payload, new MockHttpServletResponse()))
106
+ .isInstanceOf(org.springframework.web.client.ResourceAccessException.class)
107
+ .hasCauseInstanceOf(java.net.SocketTimeoutException.class);
108
+ } finally {
109
+ server.stop(0);
110
+ }
111
+ }
112
+
113
+ }
@@ -0,0 +1,273 @@
1
+ package com.epcc.arkweb.web.sensitive.reveal;
2
+
3
+ import com.epcc.arkweb.ShiroTestSupport;
4
+ import com.epcc.arkweb.helper.AuthenticatedRequest;
5
+ import com.fasterxml.jackson.databind.ObjectMapper;
6
+ import com.google.code.kaptcha.Producer;
7
+ import com.union.control.service.SensitiveDataDemoService;
8
+ import com.union.control.service.sensitive.SensitiveRevealService;
9
+ import org.apache.shiro.SecurityUtils;
10
+ import org.apache.shiro.authz.annotation.RequiresPermissions;
11
+ import org.apache.shiro.session.Session;
12
+ import org.apache.shiro.session.mgt.SimpleSession;
13
+ import org.junit.After;
14
+ import org.junit.Before;
15
+ import org.junit.Test;
16
+ import org.mockito.ArgumentCaptor;
17
+ import org.springframework.http.MediaType;
18
+ import org.springframework.test.web.servlet.MockMvc;
19
+ import org.springframework.test.web.servlet.setup.MockMvcBuilders;
20
+
21
+ import javax.imageio.ImageIO;
22
+ import java.awt.image.BufferedImage;
23
+ import java.io.ByteArrayInputStream;
24
+ import java.lang.reflect.Method;
25
+ import java.time.Clock;
26
+
27
+ import static org.assertj.core.api.Assertions.assertThat;
28
+ import static org.assertj.core.api.Assertions.assertThatThrownBy;
29
+ import static org.mockito.Matchers.anyString;
30
+ import static org.mockito.Mockito.*;
31
+ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
32
+ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
33
+
34
+ public class SensitiveRevealControllerTest {
35
+ private static final String ROOT = "/api/sensitive/reveal";
36
+ private static final String TOKEN = "rt_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
37
+ private static final String OTHER_TOKEN = "rt_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB";
38
+ private final Producer producer = mock(Producer.class);
39
+ private final SensitiveRevealService reveal = mock(SensitiveRevealService.class);
40
+ private final SensitiveDataDemoService demo = mock(SensitiveDataDemoService.class);
41
+ private final Clock clock = mock(Clock.class);
42
+ private SensitiveCaptchaService captcha;
43
+ private MockMvc mvc;
44
+
45
+ @Before
46
+ public void setUp() {
47
+ ShiroTestSupport.bindLocalUser();
48
+ when(producer.createText()).thenReturn("A7K9");
49
+ when(producer.createImage(anyString())).thenReturn(new BufferedImage(110, 40,
50
+ BufferedImage.TYPE_INT_RGB));
51
+ when(clock.millis()).thenReturn(1000L);
52
+ when(reveal.reveal(anyString())).thenReturn("13800138000");
53
+ captcha = new SensitiveCaptchaService(producer, clock);
54
+ mvc = mvc(true);
55
+ }
56
+
57
+ @After
58
+ public void tearDown() { ShiroTestSupport.clear(); }
59
+
60
+ @Test
61
+ public void normalModeKeepsDirectRevealAndDoesNotGenerateACaptcha() throws Exception {
62
+ mvc = mvc(false);
63
+ mvc.perform(get(ROOT + "/options")).andExpect(jsonPath("$.strictMode").value(false));
64
+ submit(TOKEN, null).andExpect(status().isOk())
65
+ .andExpect(jsonPath("$.value").value("13800138000"));
66
+ mvc.perform(post(ROOT + "/captcha").contentType(MediaType.APPLICATION_JSON)
67
+ .content("{\"token\":\"" + TOKEN + "\"}"))
68
+ .andExpect(status().isConflict());
69
+ verifyZeroInteractions(producer);
70
+ }
71
+
72
+ @Test
73
+ public void strictModeReturnsPngAndOnlyCallsControlAfterCorrectAnswer() throws Exception {
74
+ mvc.perform(get(ROOT + "/options")).andExpect(jsonPath("$.strictMode").value(true));
75
+ byte[] png = generate();
76
+ BufferedImage image = ImageIO.read(new ByteArrayInputStream(png));
77
+ assertThat(image.getWidth()).isEqualTo(110);
78
+ assertThat(image.getHeight()).isEqualTo(40);
79
+ verifyZeroInteractions(reveal);
80
+
81
+ submit(TOKEN, " a7k9 ").andExpect(status().isOk())
82
+ .andExpect(header().string("Cache-Control", "no-store"))
83
+ .andExpect(jsonPath("$.value").value("13800138000"));
84
+ ArgumentCaptor<String> input = ArgumentCaptor.forClass(String.class);
85
+ verify(reveal).reveal(input.capture());
86
+ assertThat(input.getValue()).contains(TOKEN, "user-1")
87
+ .doesNotContain("captchaCode").doesNotContain("a7k9");
88
+ submit(TOKEN, "A7K9").andExpect(status().isForbidden());
89
+ verifyNoMoreInteractions(reveal);
90
+ }
91
+
92
+ @Test
93
+ public void missingWrongAndExpiredAnswersNeverReachControl() throws Exception {
94
+ submit(TOKEN, null).andExpect(status().isForbidden())
95
+ .andExpect(jsonPath("$.code").value("CAPTCHA_REQUIRED"));
96
+ generate();
97
+ submit(TOKEN, "wrong").andExpect(status().isForbidden())
98
+ .andExpect(jsonPath("$.code").value("CAPTCHA_INVALID"));
99
+ submit(TOKEN, "A7K9").andExpect(status().isForbidden());
100
+ generate();
101
+ when(clock.millis()).thenReturn(121000L);
102
+ submit(TOKEN, "A7K9").andExpect(status().isForbidden())
103
+ .andExpect(jsonPath("$.code").value("CAPTCHA_EXPIRED"));
104
+ verifyZeroInteractions(reveal);
105
+ }
106
+
107
+ @Test
108
+ public void captchaCannotBeUsedForAnotherTokenOrSession() throws Exception {
109
+ generate();
110
+ submit(OTHER_TOKEN, "A7K9").andExpect(status().isForbidden());
111
+ submit(TOKEN, "A7K9").andExpect(status().isForbidden());
112
+ generate();
113
+ ShiroTestSupport.bindLocalUser();
114
+ submit(TOKEN, "A7K9").andExpect(status().isForbidden());
115
+ verifyZeroInteractions(reveal);
116
+ }
117
+
118
+ @Test
119
+ public void refreshingOverwritesThePreviousChallenge() throws Exception {
120
+ generate();
121
+ when(producer.createText()).thenReturn("B8M2");
122
+ generate();
123
+ submit(TOKEN, "A7K9").andExpect(status().isForbidden());
124
+ generate();
125
+ submit(TOKEN, "B8M2").andExpect(status().isOk());
126
+ verify(reveal, times(1)).reveal(anyString());
127
+ }
128
+
129
+ @Test
130
+ public void invalidTokenDoesNotGenerateAnImage() throws Exception {
131
+ mvc.perform(post(ROOT + "/captcha").contentType(MediaType.APPLICATION_JSON)
132
+ .content("{\"token\":\"bad\"}"))
133
+ .andExpect(status().isBadRequest());
134
+ verifyZeroInteractions(producer, reveal);
135
+ }
136
+
137
+ @Test
138
+ public void anonymousRequestsCannotGenerateAnImage() throws Exception {
139
+ SecurityUtils.getSubject().logout();
140
+ assertThatThrownBy(() -> generate()).hasRootCauseInstanceOf(
141
+ org.apache.shiro.authz.UnauthenticatedException.class);
142
+ verifyZeroInteractions(producer, reveal);
143
+ }
144
+
145
+ @Test
146
+ public void everyBrowserEndpointRetainsTheExistingPermission() {
147
+ for (Method method : SensitiveRevealController.class.getDeclaredMethods()) {
148
+ if (method.getAnnotation(org.springframework.web.bind.annotation.PostMapping.class) == null
149
+ && method.getAnnotation(org.springframework.web.bind.annotation.GetMapping.class) == null) continue;
150
+ assertThat(method.getAnnotation(RequiresPermissions.class).value())
151
+ .containsExactly("/assistantManager/page");
152
+ }
153
+ }
154
+
155
+ @Test
156
+ public void changingUserWithinASessionInvalidatesTheAnswer() {
157
+ Session session = new SimpleSession();
158
+ captcha.generate(session, "user-1", TOKEN);
159
+ assertThatThrownBy(() -> captcha.verify(session, "user-2", TOKEN, "A7K9"))
160
+ .isInstanceOf(SensitiveCaptchaService.CaptchaException.class);
161
+ }
162
+
163
+ @Test
164
+ public void actualKaptchaProducesADecodablePngAndTheAnswerIsVerifiable() throws Exception {
165
+ Producer real = spy(new com.epcc.arkweb.mock.LocalKaptchaConfiguration().captchaProducer());
166
+ doReturn("A7K9").when(real).createText();
167
+ SensitiveCaptchaService service = new SensitiveCaptchaService(real, clock);
168
+ Session session = new SimpleSession();
169
+
170
+ BufferedImage image = ImageIO.read(new ByteArrayInputStream(service.generate(session, "user-1", TOKEN)));
171
+
172
+ assertThat(image.getWidth()).isEqualTo(110);
173
+ assertThat(image.getHeight()).isEqualTo(40);
174
+ service.verify(session, "user-1", TOKEN, "A7K9");
175
+ }
176
+
177
+ @Test
178
+ public void pngFailureDoesNotLeaveTheOldChallengeUsable() throws Exception {
179
+ generate();
180
+ when(producer.createImage(anyString())).thenThrow(new IllegalStateException("internal image failure"));
181
+ mvc.perform(post(ROOT + "/captcha").contentType(MediaType.APPLICATION_JSON)
182
+ .content("{\"token\":\"" + TOKEN + "\"}"))
183
+ .andExpect(status().isServiceUnavailable())
184
+ .andExpect(jsonPath("$.code").value("CAPTCHA_UNAVAILABLE"));
185
+ submit(TOKEN, "A7K9").andExpect(status().isForbidden());
186
+ verifyZeroInteractions(reveal);
187
+ }
188
+
189
+ @Test
190
+ public void sessionChallengeSurvivesSerializationAndIsConsumedOnceUnderConcurrency() throws Exception {
191
+ SimpleSession session = new SimpleSession();
192
+ captcha.generate(session, "user-1", TOKEN);
193
+ java.io.ByteArrayOutputStream bytes = new java.io.ByteArrayOutputStream();
194
+ try (java.io.ObjectOutputStream output = new java.io.ObjectOutputStream(bytes)) {
195
+ output.writeObject(session);
196
+ }
197
+ final Session restored;
198
+ try (java.io.ObjectInputStream input = new java.io.ObjectInputStream(
199
+ new ByteArrayInputStream(bytes.toByteArray()))) {
200
+ restored = (Session) input.readObject();
201
+ }
202
+ java.util.concurrent.ExecutorService threads = java.util.concurrent.Executors.newFixedThreadPool(2);
203
+ java.util.concurrent.CountDownLatch start = new java.util.concurrent.CountDownLatch(1);
204
+ java.util.concurrent.Callable<Integer> attempt = () -> {
205
+ start.await();
206
+ try {
207
+ captcha.verify(restored, "user-1", TOKEN, "A7K9");
208
+ return 1;
209
+ } catch (SensitiveCaptchaService.CaptchaException expected) {
210
+ return 0;
211
+ }
212
+ };
213
+ try {
214
+ java.util.concurrent.Future<Integer> first = threads.submit(attempt);
215
+ java.util.concurrent.Future<Integer> second = threads.submit(attempt);
216
+ start.countDown();
217
+ assertThat(first.get(5, java.util.concurrent.TimeUnit.SECONDS)
218
+ + second.get(5, java.util.concurrent.TimeUnit.SECONDS)).isEqualTo(1);
219
+ } finally {
220
+ threads.shutdownNow();
221
+ }
222
+ }
223
+
224
+ @Test
225
+ public void savesReturnExactMaskedRowsWithoutClientRevealAndOverwriteIdentity() throws Exception {
226
+ java.util.Map<String, Object> saved = new java.util.LinkedHashMap<>();
227
+ saved.put("id", 7); saved.put("phoneNumber", "[#138****8000#VIEW:" + TOKEN + "]");
228
+ when(demo.save(anyString())).thenReturn(saved);
229
+ when(demo.saveAddressBook(anyString())).thenReturn(saved);
230
+ for (String path : new String[]{"/demo/save", "/demo/address-book/save"}) {
231
+ mvc.perform(post(ROOT + path).contentType(MediaType.APPLICATION_JSON)
232
+ .content("{\"id\":7,\"userId\":\"forged\",\"phoneNumber\":\"new text\"}"))
233
+ .andExpect(status().isOk()).andExpect(header().string("Cache-Control", "no-store"))
234
+ .andExpect(jsonPath("$.id").value(7))
235
+ .andExpect(jsonPath("$.phoneNumber").value(saved.get("phoneNumber")));
236
+ }
237
+ ArgumentCaptor<String> input = ArgumentCaptor.forClass(String.class);
238
+ verify(demo).save(input.capture());
239
+ assertThat(input.getValue()).contains("user-1").doesNotContain("forged");
240
+ verifyZeroInteractions(reveal, producer);
241
+ }
242
+
243
+ @Test
244
+ public void expiredSaveTokenHasARefreshableStatusWithoutPlaintext() throws Exception {
245
+ when(demo.save(anyString())).thenThrow(new SensitiveRevealService.ExpiredTokenException());
246
+ mvc.perform(post(ROOT + "/demo/save").contentType(MediaType.APPLICATION_JSON).content("{}"))
247
+ .andExpect(status().isGone()).andExpect(jsonPath("$.code").value("TOKEN_EXPIRED"))
248
+ .andExpect(header().string("Cache-Control", "no-store"));
249
+ verifyZeroInteractions(reveal, producer);
250
+ }
251
+
252
+ private MockMvc mvc(boolean strict) {
253
+ return MockMvcBuilders.standaloneSetup(new SensitiveRevealController(reveal,
254
+ demo, new AuthenticatedRequest(new ObjectMapper()),
255
+ captcha, strict)).build();
256
+ }
257
+
258
+ private byte[] generate() throws Exception {
259
+ return mvc.perform(post(ROOT + "/captcha").contentType(MediaType.APPLICATION_JSON)
260
+ .content("{\"token\":\"" + TOKEN + "\"}"))
261
+ .andExpect(status().isOk())
262
+ .andExpect(content().contentType(MediaType.IMAGE_PNG))
263
+ .andExpect(header().string("Cache-Control", "no-store"))
264
+ .andReturn().getResponse().getContentAsByteArray();
265
+ }
266
+
267
+ private org.springframework.test.web.servlet.ResultActions submit(String token, String answer)
268
+ throws Exception {
269
+ return mvc.perform(post(ROOT).contentType(MediaType.APPLICATION_JSON)
270
+ .content("{\"token\":\"" + token + "\""
271
+ + (answer == null ? "" : ",\"captchaCode\":\"" + answer + "\"") + "}"));
272
+ }
273
+ }
package/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "union-ark-web",
3
+ "version": "1.0.1",
4
+ "description": "Ark Web and Ark Control Dubbo applications for Union Agent",
5
+ "files": [
6
+ "ark-control-facade",
7
+ "ark-control",
8
+ "ark-web",
9
+ "!ark-control-facade/target",
10
+ "!ark-control/target",
11
+ "!ark-web/target",
12
+ "PROJECT_OVERVIEW.md",
13
+ "pom.xml"
14
+ ],
15
+ "scripts": {
16
+ "test": "mvn test"
17
+ }
18
+ }
package/pom.xml ADDED
@@ -0,0 +1,16 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project xmlns="http://maven.apache.org/POM/4.0.0"
3
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
5
+ <modelVersion>4.0.0</modelVersion>
6
+ <groupId>com.nucc.channel.ark</groupId>
7
+ <artifactId>union-agent-apps</artifactId>
8
+ <version>1.0.0</version>
9
+ <packaging>pom</packaging>
10
+
11
+ <modules>
12
+ <module>ark-control-facade</module>
13
+ <module>ark-control</module>
14
+ <module>ark-web</module>
15
+ </modules>
16
+ </project>