Skip to content

Commit b32fcc6

Browse files
larry4xienobodyiam
andauthored
feat: add observe status access-key for pre-check and logging only (#5216) (#5236)
* feat: add observe status access-key for pre-check and logging only (#5216) - ALTER TABLE `AccessKey` ADD COLUMN `Mode`, 0: filter,1: observer - portal: CRUD for observe status access-key - configservice: pre-check and logging via ClientAuthenticationFilter * changelog: add observe status access-key for pre-check and logging only (#5216) * refactor: #5236 * refactor: #5236 test code * refactor: #5236 Update apolloconfigdb.sql Co-authored-by: Jason Song <[email protected]> * changelog & refactor: #5236 --------- Co-authored-by: Jason Song <[email protected]>
1 parent 94c28af commit b32fcc6

File tree

28 files changed

+391
-59
lines changed

28 files changed

+391
-59
lines changed

CHANGES.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Apollo 2.4.0
1111
* [Fix: Resolve issues with duplicate comments and blank lines in configuration management](https://github.com/apolloconfig/apollo/pull/5232)
1212
* [Fix link namespace published items show missing some items](https://github.com/apolloconfig/apollo/pull/5240)
1313
* [Feature: Add limit and whitelist for namespace count per appid+cluster](https://github.com/apolloconfig/apollo/pull/5228)
14+
* [Feature support the observe status access-key for pre-check and logging only](https://github.com/apolloconfig/apollo/pull/5236)
1415

1516
------------------
1617
All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1)

apollo-adminservice/src/main/java/com/ctrip/framework/apollo/adminservice/controller/AccessKeyController.java

+7-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
*/
1717
package com.ctrip.framework.apollo.adminservice.controller;
1818

19+
import static com.ctrip.framework.apollo.common.constants.AccessKeyMode.FILTER;
20+
1921
import com.ctrip.framework.apollo.biz.entity.AccessKey;
2022
import com.ctrip.framework.apollo.biz.service.AccessKeyService;
2123
import com.ctrip.framework.apollo.common.dto.AccessKeyDTO;
@@ -27,6 +29,7 @@
2729
import org.springframework.web.bind.annotation.PostMapping;
2830
import org.springframework.web.bind.annotation.PutMapping;
2931
import org.springframework.web.bind.annotation.RequestBody;
32+
import org.springframework.web.bind.annotation.RequestParam;
3033
import org.springframework.web.bind.annotation.RestController;
3134

3235
/**
@@ -61,9 +64,11 @@ public void delete(@PathVariable String appId, @PathVariable long id, String ope
6164
}
6265

6366
@PutMapping(value = "/apps/{appId}/accesskeys/{id}/enable")
64-
public void enable(@PathVariable String appId, @PathVariable long id, String operator) {
67+
public void enable(@PathVariable String appId, @PathVariable long id,
68+
@RequestParam(required = false, defaultValue = "" + FILTER) int mode, String operator) {
6569
AccessKey entity = new AccessKey();
6670
entity.setId(id);
71+
entity.setMode(mode);
6772
entity.setEnabled(true);
6873
entity.setDataChangeLastModifiedBy(operator);
6974

@@ -74,6 +79,7 @@ public void enable(@PathVariable String appId, @PathVariable long id, String ope
7479
public void disable(@PathVariable String appId, @PathVariable long id, String operator) {
7580
AccessKey entity = new AccessKey();
7681
entity.setId(id);
82+
entity.setMode(FILTER);
7783
entity.setEnabled(false);
7884
entity.setDataChangeLastModifiedBy(operator);
7985

apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/entity/AccessKey.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ public class AccessKey extends BaseEntity {
3636
@Column(name = "`Secret`", nullable = false)
3737
private String secret;
3838

39+
@Column(name = "`Mode`")
40+
private int mode;
41+
3942
@Column(name = "`IsEnabled`", columnDefinition = "Bit default '0'")
4043
private boolean enabled;
4144

@@ -55,6 +58,14 @@ public void setSecret(String secret) {
5558
this.secret = secret;
5659
}
5760

61+
public int getMode() {
62+
return mode;
63+
}
64+
65+
public void setMode(int mode) {
66+
this.mode = mode;
67+
}
68+
5869
public boolean isEnabled() {
5970
return enabled;
6071
}
@@ -66,6 +77,6 @@ public void setEnabled(boolean enabled) {
6677
@Override
6778
public String toString() {
6879
return toStringHelper().add("appId", appId).add("secret", secret)
69-
.add("enabled", enabled).toString();
80+
.add("mode", mode).add("enabled", enabled).toString();
7081
}
7182
}

apollo-biz/src/main/java/com/ctrip/framework/apollo/biz/service/AccessKeyService.java

+1
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public AccessKey update(String appId, AccessKey entity) {
7474
throw BadRequestException.accessKeyNotExists();
7575
}
7676

77+
accessKey.setMode(entity.getMode());
7778
accessKey.setEnabled(entity.isEnabled());
7879
accessKey.setDataChangeLastModifiedBy(operator);
7980
accessKeyRepository.save(accessKey);

apollo-biz/src/test/resources/sql/accesskey-test.sql

+5-5
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
-- See the License for the specific language governing permissions and
1414
-- limitations under the License.
1515
--
16-
INSERT INTO "AccessKey" (`Id`, `AppId`, `Secret`, `IsEnabled`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`)
16+
INSERT INTO "AccessKey" (`Id`, `AppId`, `Secret`, `Mode`, `IsEnabled`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`)
1717
VALUES
18-
(1, 'someAppId', 'someSecret', 0, 0, 'apollo', '2019-12-19 10:28:40', 'apollo', '2019-12-19 10:28:40'),
19-
(2, '100004458', 'c715cbc80fc44171b43732c3119c9456', 0, 0, 'apollo', '2019-12-19 10:39:54', 'apollo', '2019-12-19 14:46:35'),
20-
(3, '100004458', '25a0e68d2a3941edb1ed3ab6dd0646cd', 0, 1, 'apollo', '2019-12-19 13:44:13', 'apollo', '2019-12-19 13:44:19'),
21-
(4, '100004458', '4003c4d7783443dc9870932bebf3b7fe', 0, 0, 'apollo', '2019-12-19 13:43:52', 'apollo', '2019-12-19 13:44:21');
18+
(1, 'someAppId', 'someSecret', 0, 0, 0, 'apollo', '2019-12-19 10:28:40', 'apollo', '2019-12-19 10:28:40'),
19+
(2, '100004458', 'c715cbc80fc44171b43732c3119c9456', 0, 0, 0, 'apollo', '2019-12-19 10:39:54', 'apollo', '2019-12-19 14:46:35'),
20+
(3, '100004458', '25a0e68d2a3941edb1ed3ab6dd0646cd', 0, 0, 1, 'apollo', '2019-12-19 13:44:13', 'apollo', '2019-12-19 13:44:19'),
21+
(4, '100004458', '4003c4d7783443dc9870932bebf3b7fe', 0, 0, 0, 'apollo', '2019-12-19 13:43:52', 'apollo', '2019-12-19 13:44:21');
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/*
2+
* Copyright 2024 Apollo Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
*/
17+
package com.ctrip.framework.apollo.common.constants;
18+
19+
public interface AccessKeyMode {
20+
21+
int FILTER = 0;
22+
23+
int OBSERVER = 1;
24+
25+
}

apollo-common/src/main/java/com/ctrip/framework/apollo/common/dto/AccessKeyDTO.java

+10
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public class AccessKeyDTO extends BaseDTO {
2424

2525
private String appId;
2626

27+
private Integer mode;
28+
2729
private Boolean enabled;
2830

2931
public Long getId() {
@@ -50,6 +52,14 @@ public void setAppId(String appId) {
5052
this.appId = appId;
5153
}
5254

55+
public Integer getMode() {
56+
return mode;
57+
}
58+
59+
public void setMode(Integer mode) {
60+
this.mode = mode;
61+
}
62+
5363
public Boolean getEnabled() {
5464
return enabled;
5565
}

apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilter.java

+53-15
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
package com.ctrip.framework.apollo.configservice.filter;
1818

1919
import com.ctrip.framework.apollo.biz.config.BizConfig;
20+
import com.ctrip.framework.apollo.common.utils.WebUtils;
2021
import com.ctrip.framework.apollo.configservice.util.AccessKeyUtil;
2122
import com.ctrip.framework.apollo.core.signature.Signature;
2223
import com.ctrip.framework.apollo.core.utils.StringUtils;
24+
import com.ctrip.framework.apollo.tracer.Tracer;
2325
import com.google.common.net.HttpHeaders;
2426
import java.io.IOException;
2527
import java.util.List;
@@ -70,29 +72,60 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain
7072

7173
List<String> availableSecrets = accessKeyUtil.findAvailableSecret(appId);
7274
if (!CollectionUtils.isEmpty(availableSecrets)) {
73-
String timestamp = request.getHeader(Signature.HTTP_HEADER_TIMESTAMP);
74-
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);
75-
76-
// check timestamp, valid within 1 minute
77-
if (!checkTimestamp(timestamp)) {
78-
logger.warn("Invalid timestamp. appId={},timestamp={}", appId, timestamp);
79-
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed");
75+
if (!doCheck(request, response, appId, availableSecrets, false)) {
8076
return;
8177
}
82-
83-
// check signature
84-
String uri = request.getRequestURI();
85-
String query = request.getQueryString();
86-
if (!checkAuthorization(authorization, availableSecrets, timestamp, uri, query)) {
87-
logger.warn("Invalid authorization. appId={},authorization={}", appId, authorization);
88-
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
89-
return;
78+
} else {
79+
// pre-check for observable secrets
80+
List<String> observableSecrets = accessKeyUtil.findObservableSecrets(appId);
81+
if (!CollectionUtils.isEmpty(observableSecrets)) {
82+
doCheck(request, response, appId, observableSecrets, true);
9083
}
9184
}
9285

9386
chain.doFilter(request, response);
9487
}
9588

89+
/**
90+
* Performs authentication checks(timestamp and signature) for the request.
91+
*
92+
* @param preCheck Boolean flag indicating whether this is a pre-check
93+
* @return true if authentication checks is successful, false otherwise
94+
*/
95+
private boolean doCheck(HttpServletRequest req, HttpServletResponse resp,
96+
String appId, List<String> secrets, boolean preCheck) throws IOException {
97+
98+
String timestamp = req.getHeader(Signature.HTTP_HEADER_TIMESTAMP);
99+
String authorization = req.getHeader(HttpHeaders.AUTHORIZATION);
100+
String ip = WebUtils.tryToGetClientIp(req);
101+
102+
// check timestamp, valid within 1 minute
103+
if (!checkTimestamp(timestamp)) {
104+
if (preCheck) {
105+
preCheckInvalidLogging(String.format("Invalid timestamp in pre-check. "
106+
+ "appId=%s,clientIp=%s,timestamp=%s", appId, ip, timestamp));
107+
} else {
108+
logger.warn("Invalid timestamp. appId={},clientIp={},timestamp={}", appId, ip, timestamp);
109+
resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed");
110+
return false;
111+
}
112+
}
113+
114+
// check signature
115+
if (!checkAuthorization(authorization, secrets, timestamp, req.getRequestURI(), req.getQueryString())) {
116+
if (preCheck) {
117+
preCheckInvalidLogging(String.format("Invalid authorization in pre-check. "
118+
+ "appId=%s,clientIp=%s,authorization=%s", appId, ip, authorization));
119+
} else {
120+
logger.warn("Invalid authorization. appId={},clientIp={},authorization={}", appId, ip, authorization);
121+
resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
122+
return false;
123+
}
124+
}
125+
126+
return true;
127+
}
128+
96129
@Override
97130
public void destroy() {
98131
//nothing
@@ -130,4 +163,9 @@ private boolean checkAuthorization(String authorization, List<String> availableS
130163
}
131164
return false;
132165
}
166+
167+
protected void preCheckInvalidLogging(String message) {
168+
logger.warn(message);
169+
Tracer.logEvent("Apollo.AccessKey.PreCheck", message);
170+
}
133171
}

apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/service/AccessKeyServiceWithCache.java

+11-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import com.ctrip.framework.apollo.biz.config.BizConfig;
2020
import com.ctrip.framework.apollo.biz.entity.AccessKey;
2121
import com.ctrip.framework.apollo.biz.repository.AccessKeyRepository;
22+
import com.ctrip.framework.apollo.common.constants.AccessKeyMode;
2223
import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory;
2324
import com.ctrip.framework.apollo.tracer.Tracer;
2425
import com.ctrip.framework.apollo.tracer.spi.Transaction;
@@ -37,11 +38,11 @@
3738
import java.util.concurrent.ScheduledExecutorService;
3839
import java.util.concurrent.ScheduledThreadPoolExecutor;
3940
import java.util.concurrent.TimeUnit;
41+
import java.util.function.Predicate;
4042
import java.util.stream.Collectors;
4143
import org.slf4j.Logger;
4244
import org.slf4j.LoggerFactory;
4345
import org.springframework.beans.factory.InitializingBean;
44-
import org.springframework.beans.factory.annotation.Autowired;
4546
import org.springframework.stereotype.Service;
4647
import org.springframework.util.CollectionUtils;
4748

@@ -86,13 +87,21 @@ private void initialize() {
8687
}
8788

8889
public List<String> getAvailableSecrets(String appId) {
90+
return getSecrets(appId, key -> key.isEnabled() && key.getMode() == AccessKeyMode.FILTER);
91+
}
92+
93+
public List<String> getObservableSecrets(String appId) {
94+
return getSecrets(appId, key -> key.isEnabled() && key.getMode() == AccessKeyMode.OBSERVER);
95+
}
96+
97+
public List<String> getSecrets(String appId, Predicate<AccessKey> filter) {
8998
List<AccessKey> accessKeys = accessKeyCache.get(appId);
9099
if (CollectionUtils.isEmpty(accessKeys)) {
91100
return Collections.emptyList();
92101
}
93102

94103
return accessKeys.stream()
95-
.filter(AccessKey::isEnabled)
104+
.filter(filter)
96105
.map(AccessKey::getSecret)
97106
.collect(Collectors.toList());
98107
}

apollo-configservice/src/main/java/com/ctrip/framework/apollo/configservice/util/AccessKeyUtil.java

+4
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ public List<String> findAvailableSecret(String appId) {
4646
return accessKeyServiceWithCache.getAvailableSecrets(appId);
4747
}
4848

49+
public List<String> findObservableSecrets(String appId) {
50+
return accessKeyServiceWithCache.getObservableSecrets(appId);
51+
}
52+
4953
public String extractAppIdFromRequest(HttpServletRequest request) {
5054
String appId = null;
5155
String servletPath = request.getServletPath();

apollo-configservice/src/test/java/com/ctrip/framework/apollo/configservice/filter/ClientAuthenticationFilterTest.java

+52-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
package com.ctrip.framework.apollo.configservice.filter;
1818

1919
import static org.mockito.ArgumentMatchers.any;
20+
import static org.mockito.ArgumentMatchers.anyString;
2021
import static org.mockito.Mockito.never;
22+
import static org.mockito.Mockito.spy;
2123
import static org.mockito.Mockito.times;
2224
import static org.mockito.Mockito.verify;
2325
import static org.mockito.Mockito.when;
@@ -26,6 +28,7 @@
2628
import com.ctrip.framework.apollo.configservice.util.AccessKeyUtil;
2729
import com.ctrip.framework.apollo.core.signature.Signature;
2830
import com.google.common.collect.Lists;
31+
import java.util.Collections;
2932
import java.util.List;
3033
import javax.servlet.FilterChain;
3134
import javax.servlet.http.HttpServletRequest;
@@ -58,7 +61,7 @@ public class ClientAuthenticationFilterTest {
5861

5962
@Before
6063
public void setUp() {
61-
clientAuthenticationFilter = new ClientAuthenticationFilter(bizConfig, accessKeyUtil);
64+
clientAuthenticationFilter = spy(new ClientAuthenticationFilter(bizConfig, accessKeyUtil));
6265
}
6366

6467
@Test
@@ -141,6 +144,54 @@ public void testAuthorizedSuccessfully() throws Exception {
141144

142145
clientAuthenticationFilter.doFilter(request, response, filterChain);
143146

147+
verifySuccessAndDoFilter();
148+
}
149+
150+
@Test
151+
public void testPreCheckInvalid() throws Exception {
152+
String appId = "someAppId";
153+
String availableSignature = "someSignature";
154+
List<String> secrets = Lists.newArrayList("someSecret");
155+
String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis() - 61 * 1000);
156+
String errorAuthorization = "Apollo someAppId:wrongSignature";
157+
158+
when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId);
159+
when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(Collections.emptyList());
160+
when(accessKeyUtil.findObservableSecrets(appId)).thenReturn(secrets);
161+
when(accessKeyUtil.buildSignature(any(), any(), any(), any())).thenReturn(availableSignature);
162+
when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp);
163+
when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(errorAuthorization);
164+
when(bizConfig.accessKeyAuthTimeDiffTolerance()).thenReturn(60);
165+
166+
clientAuthenticationFilter.doFilter(request, response, filterChain);
167+
168+
verifySuccessAndDoFilter();
169+
verify(clientAuthenticationFilter, times(2)).preCheckInvalidLogging(anyString());
170+
}
171+
172+
@Test
173+
public void testPreCheckSuccessfully() throws Exception {
174+
String appId = "someAppId";
175+
String availableSignature = "someSignature";
176+
List<String> secrets = Lists.newArrayList("someSecret");
177+
String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis());
178+
String correctAuthorization = "Apollo someAppId:someSignature";
179+
180+
when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId);
181+
when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(Collections.emptyList());
182+
when(accessKeyUtil.findObservableSecrets(appId)).thenReturn(secrets);
183+
when(accessKeyUtil.buildSignature(any(), any(), any(), any())).thenReturn(availableSignature);
184+
when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp);
185+
when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(correctAuthorization);
186+
when(bizConfig.accessKeyAuthTimeDiffTolerance()).thenReturn(60);
187+
188+
clientAuthenticationFilter.doFilter(request, response, filterChain);
189+
190+
verifySuccessAndDoFilter();
191+
verify(clientAuthenticationFilter, never()).preCheckInvalidLogging(anyString());
192+
}
193+
194+
private void verifySuccessAndDoFilter() throws Exception {
144195
verify(response, never()).sendError(HttpServletResponse.SC_BAD_REQUEST, "InvalidAppId");
145196
verify(response, never()).sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed");
146197
verify(response, never()).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");

apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/api/AdminServiceAPI.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -317,9 +317,9 @@ public void delete(Env env, String appId, long id, String operator) {
317317
}
318318

319319
@ApolloAuditLog(type = OpType.RPC, name = "AccessKey.enableInRemote")
320-
public void enable(Env env, String appId, long id, String operator) {
321-
restTemplate.put(env, "apps/{appId}/accesskeys/{id}/enable?operator={operator}",
322-
null, appId, id, operator);
320+
public void enable(Env env, String appId, long id, int mode, String operator) {
321+
restTemplate.put(env, "apps/{appId}/accesskeys/{id}/enable?mode={mode}&operator={operator}",
322+
null, appId, id, mode, operator);
323323
}
324324

325325
@ApolloAuditLog(type = OpType.RPC, name = "AccessKey.disableInRemote")

0 commit comments

Comments
 (0)