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.
- package/PROJECT_OVERVIEW.md +405 -0
- package/README.md +177 -0
- package/ark-control/pom.xml +142 -0
- package/ark-control/src/local-mock/java/com/epcc/commons/securityproxy/api/SecurityResult.java +14 -0
- package/ark-control/src/local-mock/java/com/epcc/commons/securityproxy/api/SymmetricalSecurityService.java +11 -0
- package/ark-control/src/local-mock/java/com/epcc/dubbo/result/Result.java +29 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/annotation/EnDecryptField.java +16 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/annotation/EnDecryptFieldLong.java +17 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/annotation/EnDecryptFieldWithTag.java +16 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/exception/BaseDataErrorCode.java +26 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/exception/CheckException.java +33 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/exception/ErrorCode.java +9 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/redis/RedisCacheService.java +131 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/util/Constant.java +24 -0
- package/ark-control/src/local-mock/java/com/nucc/channel/ark/common/util/ResultUtil.java +9 -0
- package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalMockSensitiveProxy.java +40 -0
- package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalRedisCacheService.java +45 -0
- package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalSensitiveHostConfiguration.java +27 -0
- package/ark-control/src/local-mock/java/com/union/control/local/sensitive/LocalSensitiveProxyConfiguration.java +16 -0
- package/ark-control/src/main/java/com/union/control/ArkControlApplication.java +17 -0
- package/ark-control/src/main/java/com/union/control/dubbo/ProviderAccessLogFilter.java +45 -0
- package/ark-control/src/main/java/com/union/control/mapper/AgentExecutionMapper.java +35 -0
- package/ark-control/src/main/java/com/union/control/mapper/ConversationMapper.java +44 -0
- package/ark-control/src/main/java/com/union/control/mapper/MemoryStoreMapper.java +41 -0
- package/ark-control/src/main/java/com/union/control/mapper/ScheduledTaskMapper.java +96 -0
- package/ark-control/src/main/java/com/union/control/mapper/SensitiveAddressBookDemo.java +31 -0
- package/ark-control/src/main/java/com/union/control/mapper/SensitiveDataDemoMapper.java +24 -0
- package/ark-control/src/main/java/com/union/control/mapper/interceptor/AESInterceptor.java +265 -0
- package/ark-control/src/main/java/com/union/control/mapper/interceptor/AddressBookHandler.java +105 -0
- package/ark-control/src/main/java/com/union/control/schedule/ScheduledExecutionToken.java +55 -0
- package/ark-control/src/main/java/com/union/control/schedule/ScheduledTaskScheduler.java +206 -0
- package/ark-control/src/main/java/com/union/control/service/impl/AgentExecutionServiceImpl.java +212 -0
- package/ark-control/src/main/java/com/union/control/service/impl/AgentProxyServiceImpl.java +143 -0
- package/ark-control/src/main/java/com/union/control/service/impl/ConversationServiceImpl.java +245 -0
- package/ark-control/src/main/java/com/union/control/service/impl/MemoryStoreServiceImpl.java +252 -0
- package/ark-control/src/main/java/com/union/control/service/impl/RunningAnalysisMockServiceImpl.java +301 -0
- package/ark-control/src/main/java/com/union/control/service/impl/ScheduledTaskServiceImpl.java +576 -0
- package/ark-control/src/main/java/com/union/control/service/impl/SensitiveDataDemoServiceImpl.java +144 -0
- package/ark-control/src/main/java/com/union/control/service/sensitive/RedisRevealTokenStore.java +90 -0
- package/ark-control/src/main/java/com/union/control/service/sensitive/SensitiveRevealProcessor.java +319 -0
- package/ark-control/src/main/java/com/union/control/service/sensitive/SensitiveRevealServiceImpl.java +72 -0
- package/ark-control/src/main/java/com/union/control/utils/AgentSupport.java +103 -0
- package/ark-control/src/main/java/com/union/control/utils/security/SymmetricalSecurityUtils.java +160 -0
- package/ark-control/src/main/resources/META-INF/dubbo/com.alibaba.dubbo.rpc.Filter +1 -0
- package/ark-control/src/main/resources/application.properties +4 -0
- package/ark-control/src/main/resources/application.yml +32 -0
- package/ark-control/src/main/resources/dubbo-provider.xml +20 -0
- package/ark-control/src/main/resources/mapper/AgentExecutionMapper.xml +75 -0
- package/ark-control/src/main/resources/mapper/ConversationMapper.xml +110 -0
- package/ark-control/src/main/resources/mapper/MemoryStoreMapper.xml +74 -0
- package/ark-control/src/main/resources/mapper/ScheduledTaskMapper.xml +286 -0
- package/ark-control/src/main/resources/mapper/SensitiveDataDemoMapper.xml +55 -0
- package/ark-control/src/main/resources/schema.sql +146 -0
- package/ark-control/src/test/java/com/union/control/AgentExecutionServiceTest.java +170 -0
- package/ark-control/src/test/java/com/union/control/ArkControlStructureContractTest.java +107 -0
- package/ark-control/src/test/java/com/union/control/ConversationServiceTest.java +89 -0
- package/ark-control/src/test/java/com/union/control/MemoryStoreServiceTest.java +26 -0
- package/ark-control/src/test/java/com/union/control/PydanticAiControlContractTest.java +52 -0
- package/ark-control/src/test/java/com/union/control/TestJson.java +45 -0
- package/ark-control/src/test/java/com/union/control/dubbo/ProviderAccessLogFilterTest.java +53 -0
- package/ark-control/src/test/java/com/union/control/local/sensitive/LocalSensitiveProxyConfigurationTest.java +23 -0
- package/ark-control/src/test/java/com/union/control/mapper/interceptor/AESInterceptorCompatibilityTest.java +505 -0
- package/ark-control/src/test/java/com/union/control/schedule/ScheduledExecutionTokenTest.java +27 -0
- package/ark-control/src/test/java/com/union/control/schedule/ScheduledTaskSchedulerTest.java +51 -0
- package/ark-control/src/test/java/com/union/control/service/AgentProxyServiceTest.java +111 -0
- package/ark-control/src/test/java/com/union/control/service/ScheduledTaskServiceTest.java +483 -0
- package/ark-control/src/test/java/com/union/control/service/impl/SensitiveDataDemoServiceImplTest.java +104 -0
- package/ark-control/src/test/java/com/union/control/service/sensitive/RedisRevealTokenStoreTest.java +65 -0
- package/ark-control/src/test/java/com/union/control/service/sensitive/SensitiveRevealBoundaryTest.java +99 -0
- package/ark-control/src/test/java/com/union/control/service/sensitive/SensitiveRevealProcessorCodecTest.java +111 -0
- package/ark-control/src/test/java/com/union/control/service/sensitive/SensitiveRevealProcessorTest.java +111 -0
- package/ark-control/src/test/java/com/union/control/utils/security/SymmetricalSecurityUtilsCompatibilityTest.java +87 -0
- package/ark-control-facade/pom.xml +16 -0
- package/ark-control-facade/src/main/java/com/union/control/service/AgentExecutionService.java +8 -0
- package/ark-control-facade/src/main/java/com/union/control/service/AgentProxyService.java +7 -0
- package/ark-control-facade/src/main/java/com/union/control/service/AgentResponse.java +22 -0
- package/ark-control-facade/src/main/java/com/union/control/service/ConversationService.java +12 -0
- package/ark-control-facade/src/main/java/com/union/control/service/MemoryStoreService.java +12 -0
- package/ark-control-facade/src/main/java/com/union/control/service/RunningAnalysisMockService.java +10 -0
- package/ark-control-facade/src/main/java/com/union/control/service/ScheduledTaskService.java +40 -0
- package/ark-control-facade/src/main/java/com/union/control/service/SensitiveDataDemoService.java +13 -0
- package/ark-control-facade/src/main/java/com/union/control/service/sensitive/SensitiveRevealService.java +18 -0
- package/ark-web/README.md +80 -0
- package/ark-web/pom.xml +105 -0
- package/ark-web/src/local-mock/java/com/epcc/arkweb/mock/ArkAuthServiceMock.java +105 -0
- package/ark-web/src/local-mock/java/com/epcc/arkweb/mock/LocalKaptchaConfiguration.java +26 -0
- package/ark-web/src/main/java/com/epcc/arkweb/Application.java +13 -0
- package/ark-web/src/main/java/com/epcc/arkweb/config/InterceptorConfig.java +20 -0
- package/ark-web/src/main/java/com/epcc/arkweb/config/Realm.java +64 -0
- package/ark-web/src/main/java/com/epcc/arkweb/config/ShiroConfig.java +129 -0
- package/ark-web/src/main/java/com/epcc/arkweb/filter/LoginFormFilter.java +43 -0
- package/ark-web/src/main/java/com/epcc/arkweb/helper/AuthContextHolder.java +39 -0
- package/ark-web/src/main/java/com/epcc/arkweb/helper/AuthenticatedRequest.java +91 -0
- package/ark-web/src/main/java/com/epcc/arkweb/model/ShiroUser.java +21 -0
- package/ark-web/src/main/java/com/epcc/arkweb/utils/ResultMsg.java +19 -0
- package/ark-web/src/main/java/com/epcc/arkweb/vo/llm/ScheduledTaskCommandVO.java +30 -0
- package/ark-web/src/main/java/com/epcc/arkweb/vo/llm/ScheduledTaskQueryVO.java +20 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/CommonController.java +25 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/llm/AgentAuthorizationInterceptor.java +229 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/llm/AgentController.java +165 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/llm/AgentPermission.java +13 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/llm/LlmController.java +209 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/llm/ScheduledTaskController.java +160 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/sensitive/reveal/SensitiveCaptchaService.java +146 -0
- package/ark-web/src/main/java/com/epcc/arkweb/web/sensitive/reveal/SensitiveRevealController.java +163 -0
- package/ark-web/src/main/resources/application.properties +1 -0
- package/ark-web/src/main/resources/application.yml +23 -0
- package/ark-web/src/main/resources/dubbo-consumer.xml +19 -0
- package/ark-web/src/production-overlay/java/com/epcc/arkweb/config/CasShiroConfig.java +229 -0
- package/ark-web/src/production-overlay/java/com/epcc/arkweb/config/InterceptorConfig.java +35 -0
- package/ark-web/src/production-overlay/java/com/epcc/arkweb/config/ShiroConfig.java +161 -0
- package/ark-web/src/test/java/com/epcc/arkweb/ArkWebStructureContractTest.java +290 -0
- package/ark-web/src/test/java/com/epcc/arkweb/ShiroTestSupport.java +32 -0
- package/ark-web/src/test/java/com/epcc/arkweb/config/LocalShiroIntegrationTest.java +121 -0
- package/ark-web/src/test/java/com/epcc/arkweb/helper/AuthenticatedRequestTest.java +131 -0
- package/ark-web/src/test/java/com/epcc/arkweb/local/LocalArkAuthServiceMockTest.java +17 -0
- package/ark-web/src/test/java/com/epcc/arkweb/web/llm/AgentAuthorizationInterceptorTest.java +134 -0
- package/ark-web/src/test/java/com/epcc/arkweb/web/llm/AgentControllerAuthorizationTest.java +97 -0
- package/ark-web/src/test/java/com/epcc/arkweb/web/llm/LlmControllerTest.java +113 -0
- package/ark-web/src/test/java/com/epcc/arkweb/web/sensitive/reveal/SensitiveRevealControllerTest.java +273 -0
- package/package.json +18 -0
- 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>
|