Skip to content

Commit

Permalink
feat: add observe status access-key for pre-check and logging only (#…
Browse files Browse the repository at this point in the history
…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 <nobodyiam@gmail.com>

* changelog & refactor: #5236

---------

Co-authored-by: Jason Song <nobodyiam@gmail.com>
  • Loading branch information
larry4xie and nobodyiam authored Oct 8, 2024
1 parent 94c28af commit b32fcc6
Show file tree
Hide file tree
Showing 28 changed files with 391 additions and 59 deletions.
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Apollo 2.4.0
* [Fix: Resolve issues with duplicate comments and blank lines in configuration management](https://github.com/apolloconfig/apollo/pull/5232)
* [Fix link namespace published items show missing some items](https://github.com/apolloconfig/apollo/pull/5240)
* [Feature: Add limit and whitelist for namespace count per appid+cluster](https://github.com/apolloconfig/apollo/pull/5228)
* [Feature support the observe status access-key for pre-check and logging only](https://github.com/apolloconfig/apollo/pull/5236)

------------------
All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/15?closed=1)
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
*/
package com.ctrip.framework.apollo.adminservice.controller;

import static com.ctrip.framework.apollo.common.constants.AccessKeyMode.FILTER;

import com.ctrip.framework.apollo.biz.entity.AccessKey;
import com.ctrip.framework.apollo.biz.service.AccessKeyService;
import com.ctrip.framework.apollo.common.dto.AccessKeyDTO;
Expand All @@ -27,6 +29,7 @@
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
Expand Down Expand Up @@ -61,9 +64,11 @@ public void delete(@PathVariable String appId, @PathVariable long id, String ope
}

@PutMapping(value = "/apps/{appId}/accesskeys/{id}/enable")
public void enable(@PathVariable String appId, @PathVariable long id, String operator) {
public void enable(@PathVariable String appId, @PathVariable long id,
@RequestParam(required = false, defaultValue = "" + FILTER) int mode, String operator) {
AccessKey entity = new AccessKey();
entity.setId(id);
entity.setMode(mode);
entity.setEnabled(true);
entity.setDataChangeLastModifiedBy(operator);

Expand All @@ -74,6 +79,7 @@ public void enable(@PathVariable String appId, @PathVariable long id, String ope
public void disable(@PathVariable String appId, @PathVariable long id, String operator) {
AccessKey entity = new AccessKey();
entity.setId(id);
entity.setMode(FILTER);
entity.setEnabled(false);
entity.setDataChangeLastModifiedBy(operator);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ public class AccessKey extends BaseEntity {
@Column(name = "`Secret`", nullable = false)
private String secret;

@Column(name = "`Mode`")
private int mode;

@Column(name = "`IsEnabled`", columnDefinition = "Bit default '0'")
private boolean enabled;

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

public int getMode() {
return mode;
}

public void setMode(int mode) {
this.mode = mode;
}

public boolean isEnabled() {
return enabled;
}
Expand All @@ -66,6 +77,6 @@ public void setEnabled(boolean enabled) {
@Override
public String toString() {
return toStringHelper().add("appId", appId).add("secret", secret)
.add("enabled", enabled).toString();
.add("mode", mode).add("enabled", enabled).toString();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ public AccessKey update(String appId, AccessKey entity) {
throw BadRequestException.accessKeyNotExists();
}

accessKey.setMode(entity.getMode());
accessKey.setEnabled(entity.isEnabled());
accessKey.setDataChangeLastModifiedBy(operator);
accessKeyRepository.save(accessKey);
Expand Down
10 changes: 5 additions & 5 deletions apollo-biz/src/test/resources/sql/accesskey-test.sql
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
-- See the License for the specific language governing permissions and
-- limitations under the License.
--
INSERT INTO "AccessKey" (`Id`, `AppId`, `Secret`, `IsEnabled`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`)
INSERT INTO "AccessKey" (`Id`, `AppId`, `Secret`, `Mode`, `IsEnabled`, `IsDeleted`, `DataChange_CreatedBy`, `DataChange_CreatedTime`, `DataChange_LastModifiedBy`, `DataChange_LastTime`)
VALUES
(1, 'someAppId', 'someSecret', 0, 0, 'apollo', '2019-12-19 10:28:40', 'apollo', '2019-12-19 10:28:40'),
(2, '100004458', 'c715cbc80fc44171b43732c3119c9456', 0, 0, 'apollo', '2019-12-19 10:39:54', 'apollo', '2019-12-19 14:46:35'),
(3, '100004458', '25a0e68d2a3941edb1ed3ab6dd0646cd', 0, 1, 'apollo', '2019-12-19 13:44:13', 'apollo', '2019-12-19 13:44:19'),
(4, '100004458', '4003c4d7783443dc9870932bebf3b7fe', 0, 0, 'apollo', '2019-12-19 13:43:52', 'apollo', '2019-12-19 13:44:21');
(1, 'someAppId', 'someSecret', 0, 0, 0, 'apollo', '2019-12-19 10:28:40', 'apollo', '2019-12-19 10:28:40'),
(2, '100004458', 'c715cbc80fc44171b43732c3119c9456', 0, 0, 0, 'apollo', '2019-12-19 10:39:54', 'apollo', '2019-12-19 14:46:35'),
(3, '100004458', '25a0e68d2a3941edb1ed3ab6dd0646cd', 0, 0, 1, 'apollo', '2019-12-19 13:44:13', 'apollo', '2019-12-19 13:44:19'),
(4, '100004458', '4003c4d7783443dc9870932bebf3b7fe', 0, 0, 0, 'apollo', '2019-12-19 13:43:52', 'apollo', '2019-12-19 13:44:21');
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2024 Apollo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.ctrip.framework.apollo.common.constants;

public interface AccessKeyMode {

int FILTER = 0;

int OBSERVER = 1;

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public class AccessKeyDTO extends BaseDTO {

private String appId;

private Integer mode;

private Boolean enabled;

public Long getId() {
Expand All @@ -50,6 +52,14 @@ public void setAppId(String appId) {
this.appId = appId;
}

public Integer getMode() {
return mode;
}

public void setMode(Integer mode) {
this.mode = mode;
}

public Boolean getEnabled() {
return enabled;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
package com.ctrip.framework.apollo.configservice.filter;

import com.ctrip.framework.apollo.biz.config.BizConfig;
import com.ctrip.framework.apollo.common.utils.WebUtils;
import com.ctrip.framework.apollo.configservice.util.AccessKeyUtil;
import com.ctrip.framework.apollo.core.signature.Signature;
import com.ctrip.framework.apollo.core.utils.StringUtils;
import com.ctrip.framework.apollo.tracer.Tracer;
import com.google.common.net.HttpHeaders;
import java.io.IOException;
import java.util.List;
Expand Down Expand Up @@ -70,29 +72,60 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain

List<String> availableSecrets = accessKeyUtil.findAvailableSecret(appId);
if (!CollectionUtils.isEmpty(availableSecrets)) {
String timestamp = request.getHeader(Signature.HTTP_HEADER_TIMESTAMP);
String authorization = request.getHeader(HttpHeaders.AUTHORIZATION);

// check timestamp, valid within 1 minute
if (!checkTimestamp(timestamp)) {
logger.warn("Invalid timestamp. appId={},timestamp={}", appId, timestamp);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed");
if (!doCheck(request, response, appId, availableSecrets, false)) {
return;
}

// check signature
String uri = request.getRequestURI();
String query = request.getQueryString();
if (!checkAuthorization(authorization, availableSecrets, timestamp, uri, query)) {
logger.warn("Invalid authorization. appId={},authorization={}", appId, authorization);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return;
} else {
// pre-check for observable secrets
List<String> observableSecrets = accessKeyUtil.findObservableSecrets(appId);
if (!CollectionUtils.isEmpty(observableSecrets)) {
doCheck(request, response, appId, observableSecrets, true);
}
}

chain.doFilter(request, response);
}

/**
* Performs authentication checks(timestamp and signature) for the request.
*
* @param preCheck Boolean flag indicating whether this is a pre-check
* @return true if authentication checks is successful, false otherwise
*/
private boolean doCheck(HttpServletRequest req, HttpServletResponse resp,
String appId, List<String> secrets, boolean preCheck) throws IOException {

String timestamp = req.getHeader(Signature.HTTP_HEADER_TIMESTAMP);
String authorization = req.getHeader(HttpHeaders.AUTHORIZATION);
String ip = WebUtils.tryToGetClientIp(req);

// check timestamp, valid within 1 minute
if (!checkTimestamp(timestamp)) {
if (preCheck) {
preCheckInvalidLogging(String.format("Invalid timestamp in pre-check. "
+ "appId=%s,clientIp=%s,timestamp=%s", appId, ip, timestamp));
} else {
logger.warn("Invalid timestamp. appId={},clientIp={},timestamp={}", appId, ip, timestamp);
resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed");
return false;
}
}

// check signature
if (!checkAuthorization(authorization, secrets, timestamp, req.getRequestURI(), req.getQueryString())) {
if (preCheck) {
preCheckInvalidLogging(String.format("Invalid authorization in pre-check. "
+ "appId=%s,clientIp=%s,authorization=%s", appId, ip, authorization));
} else {
logger.warn("Invalid authorization. appId={},clientIp={},authorization={}", appId, ip, authorization);
resp.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
return false;
}
}

return true;
}

@Override
public void destroy() {
//nothing
Expand Down Expand Up @@ -130,4 +163,9 @@ private boolean checkAuthorization(String authorization, List<String> availableS
}
return false;
}

protected void preCheckInvalidLogging(String message) {
logger.warn(message);
Tracer.logEvent("Apollo.AccessKey.PreCheck", message);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.ctrip.framework.apollo.biz.config.BizConfig;
import com.ctrip.framework.apollo.biz.entity.AccessKey;
import com.ctrip.framework.apollo.biz.repository.AccessKeyRepository;
import com.ctrip.framework.apollo.common.constants.AccessKeyMode;
import com.ctrip.framework.apollo.core.utils.ApolloThreadFactory;
import com.ctrip.framework.apollo.tracer.Tracer;
import com.ctrip.framework.apollo.tracer.spi.Transaction;
Expand All @@ -37,11 +38,11 @@
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

Expand Down Expand Up @@ -86,13 +87,21 @@ private void initialize() {
}

public List<String> getAvailableSecrets(String appId) {
return getSecrets(appId, key -> key.isEnabled() && key.getMode() == AccessKeyMode.FILTER);
}

public List<String> getObservableSecrets(String appId) {
return getSecrets(appId, key -> key.isEnabled() && key.getMode() == AccessKeyMode.OBSERVER);
}

public List<String> getSecrets(String appId, Predicate<AccessKey> filter) {
List<AccessKey> accessKeys = accessKeyCache.get(appId);
if (CollectionUtils.isEmpty(accessKeys)) {
return Collections.emptyList();
}

return accessKeys.stream()
.filter(AccessKey::isEnabled)
.filter(filter)
.map(AccessKey::getSecret)
.collect(Collectors.toList());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public List<String> findAvailableSecret(String appId) {
return accessKeyServiceWithCache.getAvailableSecrets(appId);
}

public List<String> findObservableSecrets(String appId) {
return accessKeyServiceWithCache.getObservableSecrets(appId);
}

public String extractAppIdFromRequest(HttpServletRequest request) {
String appId = null;
String servletPath = request.getServletPath();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
package com.ctrip.framework.apollo.configservice.filter;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
Expand All @@ -26,6 +28,7 @@
import com.ctrip.framework.apollo.configservice.util.AccessKeyUtil;
import com.ctrip.framework.apollo.core.signature.Signature;
import com.google.common.collect.Lists;
import java.util.Collections;
import java.util.List;
import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
Expand Down Expand Up @@ -58,7 +61,7 @@ public class ClientAuthenticationFilterTest {

@Before
public void setUp() {
clientAuthenticationFilter = new ClientAuthenticationFilter(bizConfig, accessKeyUtil);
clientAuthenticationFilter = spy(new ClientAuthenticationFilter(bizConfig, accessKeyUtil));
}

@Test
Expand Down Expand Up @@ -141,6 +144,54 @@ public void testAuthorizedSuccessfully() throws Exception {

clientAuthenticationFilter.doFilter(request, response, filterChain);

verifySuccessAndDoFilter();
}

@Test
public void testPreCheckInvalid() throws Exception {
String appId = "someAppId";
String availableSignature = "someSignature";
List<String> secrets = Lists.newArrayList("someSecret");
String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis() - 61 * 1000);
String errorAuthorization = "Apollo someAppId:wrongSignature";

when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId);
when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(Collections.emptyList());
when(accessKeyUtil.findObservableSecrets(appId)).thenReturn(secrets);
when(accessKeyUtil.buildSignature(any(), any(), any(), any())).thenReturn(availableSignature);
when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp);
when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(errorAuthorization);
when(bizConfig.accessKeyAuthTimeDiffTolerance()).thenReturn(60);

clientAuthenticationFilter.doFilter(request, response, filterChain);

verifySuccessAndDoFilter();
verify(clientAuthenticationFilter, times(2)).preCheckInvalidLogging(anyString());
}

@Test
public void testPreCheckSuccessfully() throws Exception {
String appId = "someAppId";
String availableSignature = "someSignature";
List<String> secrets = Lists.newArrayList("someSecret");
String oneMinAgoTimestamp = Long.toString(System.currentTimeMillis());
String correctAuthorization = "Apollo someAppId:someSignature";

when(accessKeyUtil.extractAppIdFromRequest(any())).thenReturn(appId);
when(accessKeyUtil.findAvailableSecret(appId)).thenReturn(Collections.emptyList());
when(accessKeyUtil.findObservableSecrets(appId)).thenReturn(secrets);
when(accessKeyUtil.buildSignature(any(), any(), any(), any())).thenReturn(availableSignature);
when(request.getHeader(Signature.HTTP_HEADER_TIMESTAMP)).thenReturn(oneMinAgoTimestamp);
when(request.getHeader(HttpHeaders.AUTHORIZATION)).thenReturn(correctAuthorization);
when(bizConfig.accessKeyAuthTimeDiffTolerance()).thenReturn(60);

clientAuthenticationFilter.doFilter(request, response, filterChain);

verifySuccessAndDoFilter();
verify(clientAuthenticationFilter, never()).preCheckInvalidLogging(anyString());
}

private void verifySuccessAndDoFilter() throws Exception {
verify(response, never()).sendError(HttpServletResponse.SC_BAD_REQUEST, "InvalidAppId");
verify(response, never()).sendError(HttpServletResponse.SC_UNAUTHORIZED, "RequestTimeTooSkewed");
verify(response, never()).sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -317,9 +317,9 @@ public void delete(Env env, String appId, long id, String operator) {
}

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

@ApolloAuditLog(type = OpType.RPC, name = "AccessKey.disableInRemote")
Expand Down
Loading

0 comments on commit b32fcc6

Please sign in to comment.