Skip to content

Commit

Permalink
feat: revoke personal access token (#2422)
Browse files Browse the repository at this point in the history
* controller and apiml PAT provider

Signed-off-by: achmelo <[email protected]>

* cachingservice client to support token operations

Signed-off-by: achmelo <[email protected]>

* list invalidated tokens

Signed-off-by: achmelo <[email protected]>

* new cache storage operation test

Signed-off-by: achmelo <[email protected]>

* remove unused import

Signed-off-by: achmelo <[email protected]>

* validate token against stored list

Signed-off-by: achmelo <[email protected]>

* remove unused import

Signed-off-by: achmelo <[email protected]>

* deserialize response body

Signed-off-by: achmelo <[email protected]>

* Include utils folder for coverage

Signed-off-by: at670475 <[email protected]>

* Add method to retrieve invalidated tokens

Signed-off-by: at670475 <[email protected]>

* Add unit tests

Signed-off-by: at670475 <[email protected]>

* Change endpoint path

Signed-off-by: at670475 <[email protected]>

* Fix null pointer exc. and add conflict exception

Signed-off-by: at670475 <[email protected]>

* fix path in the it

Signed-off-by: at670475 <[email protected]>

* add unit test

Signed-off-by: at670475 <[email protected]>

* object mapper config

Signed-off-by: achmelo <[email protected]>

* Address review comments

Signed-off-by: at670475 <[email protected]>

* Unit test

Signed-off-by: at670475 <[email protected]>

* argon2 hash, validate tokens

Signed-off-by: achmelo <[email protected]>

* revoke token through GW test

Signed-off-by: achmelo <[email protected]>

* add licence info

Signed-off-by: achmelo <[email protected]>

* remove unused imports

Signed-off-by: achmelo <[email protected]>

* fix styles

Signed-off-by: achmelo <[email protected]>

* exclude infinispan storage tests from base

Signed-off-by: achmelo <[email protected]>

* cs list path

Signed-off-by: achmelo <[email protected]>

* Use synchronized and add it

Signed-off-by: at670475 <[email protected]>

* Add it

Signed-off-by: at670475 <[email protected]>

* check for array size

Signed-off-by: achmelo <[email protected]>

* use different key for CSstorage test

Signed-off-by: achmelo <[email protected]>

* replace transaction with lock

Signed-off-by: achmelo <[email protected]>

* exclude infinispan tests from CITests

Signed-off-by: achmelo <[email protected]>

* chore

Signed-off-by: achmelo <[email protected]>

* Rename functions + clean up of exception + fix sonarlint

Signed-off-by: at670475 <[email protected]>

* Refactoring it

Signed-off-by: at670475 <[email protected]>

* add test for untrusted cert

Signed-off-by: at670475 <[email protected]>

* remove unnecessary header

Signed-off-by: at670475 <[email protected]>

* fix iT

Signed-off-by: at670475 <[email protected]>

* remove redundant threads

Signed-off-by: achmelo <[email protected]>

* verify that invalid token can't be revoked

Signed-off-by: achmelo <[email protected]>

* do not include userId, unit tests

Signed-off-by: achmelo <[email protected]>

* replace list with map

Signed-off-by: achmelo <[email protected]>

* Add uncompatible storage method ex. handling

Signed-off-by: at670475 <[email protected]>

* store salt for hash

Signed-off-by: achmelo <[email protected]>

* Fix unit tests and integration tests

Signed-off-by: at670475 <[email protected]>

* Remove configuration for the transaction manager as not used

Signed-off-by: at670475 <[email protected]>

* chore

Signed-off-by: achmelo <[email protected]>

* Revert "Remove configuration for the transaction manager as not used"

This reverts commit 4c8eb80.

* replace lambda

Signed-off-by: achmelo <[email protected]>

* Fix unit tests

Signed-off-by: at670475 <[email protected]>

* Add unit test and fix code smells

Signed-off-by: at670475 <[email protected]>

* fix code smells

Signed-off-by: at670475 <[email protected]>

* fix message action

Signed-off-by: at670475 <[email protected]>

* remove unused argument

Signed-off-by: achmelo <[email protected]>

* increase coverage

Signed-off-by: at670475 <[email protected]>

Co-authored-by: Andrea Tabone <[email protected]>
Co-authored-by: Andrea Tabone <[email protected]>
  • Loading branch information
3 people authored Jun 10, 2022
1 parent b083dd3 commit c7f79d5
Show file tree
Hide file tree
Showing 32 changed files with 1,240 additions and 312 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/containers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1022,7 +1022,7 @@ jobs:

- name: Build with Gradle
run: >
./gradlew :integration-tests:runCachingServiceTests --info -Denvironment.config=-docker -Denvironment.offPlatform=true
./gradlew :integration-tests:runInfinispanServiceTests --info -Denvironment.config=-docker -Denvironment.offPlatform=true
-Partifactory_user=${{ secrets.ARTIFACTORY_USERNAME }} -Partifactory_password=${{ secrets.ARTIFACTORY_PASSWORD }}
- uses: ./.github/actions/dump-jacoco
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* This program and the accompanying materials are made available under the terms of the
* Eclipse Public License v2.0 which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-v20.html
*
* SPDX-License-Identifier: EPL-2.0
*
* Copyright Contributors to the Zowe Project.
*/
package org.zowe.apiml.security.common.token;

public interface AccessTokenProvider {

void invalidateToken(String token) throws Exception;
boolean isInvalidated(String token) throws Exception;
}
6 changes: 5 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,11 @@ task runBaseTestsInternalPort(dependsOn: ":integration-tests:runBaseTestsInterna
group "Integration tests"
}
task runCachingServiceTests(dependsOn: ":integration-tests:runCachingServiceTests") {
description "Run base tests on internal port"
description "Run caching service tests"
group "Integration tests"
}
task runInfinispanServiceTests(dependsOn: ":integration-tests:runInfinispanServiceTests") {
description "Run tests for caching service with infinispan storage option"
group "Integration tests"
}
task runHATests(dependsOn: ":integration-tests:runHATests") {
Expand Down
3 changes: 3 additions & 0 deletions caching-service/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,15 @@ dependencies {
implementation project(':common-service-core')
implementation project(':apiml-tomcat-common')
implementation project(':onboarding-enabler-spring')


implementation libraries.zowe_attls
implementation libraries.jjwt
implementation libraries.jjwt_impl
implementation libraries.jjwt_jackson
implementation libraries.infinispan_core
implementation libraries.infinispan_jboss_marshalling
implementation libraries.infinispan_lock

implementation libraries.springFox
implementation libraries.spring_boot_starter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,33 @@ public ResponseEntity<Object> createKey(@RequestBody KeyValue keyValue, HttpServ
keyValue, request, HttpStatus.CREATED);
}

@PostMapping(value = "/cache-list", produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "Add a new list item in the cache",
notes = "A new key-value pair will be added to the cache")
@ResponseBody
@HystrixCommand
public ResponseEntity<Object> storeListItem(@RequestBody KeyValue keyValue, HttpServletRequest request) {
return keyValueRequest(storage::storeListItem,
keyValue, request, HttpStatus.CREATED);
}

@GetMapping(value = "/cache-list/{key}", produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "Retrieves all the list items in the cache",
notes = "Values returned for the calling service")
@ResponseBody
@HystrixCommand
public ResponseEntity<Object> getAllListItems(@PathVariable String key, HttpServletRequest request) {
return getServiceId(request).<ResponseEntity<Object>>map(
s -> {
try {
return new ResponseEntity<>(storage.getAllMapItems(s, key), HttpStatus.OK);
} catch (Exception exception) {
return handleIncompatibleStorageMethod(exception, request.getRequestURL());
}
}
).orElseGet(this::getUnauthorizedResponse);
}

@PutMapping(value = "/cache", produces = MediaType.APPLICATION_JSON_VALUE)
@ApiOperation(value = "Update key in the cache",
notes = "Value at the key in the provided key-value pair will be updated to the provided value")
Expand Down Expand Up @@ -202,6 +229,12 @@ private ResponseEntity<Object> handleInternalError(Exception exception, StringBu
return new ResponseEntity<>(message.mapToView(), internalServerError.getStatus());
}

private ResponseEntity<Object> handleIncompatibleStorageMethod(Exception exception, StringBuffer requestURL) {
Messages internalServerError = Messages.INCOMPATIBLE_STORAGE_METHOD;
Message message = messageService.createMessage(internalServerError.getKey(), requestURL, exception.getMessage(), exception.toString());
return new ResponseEntity<>(message.mapToView(), internalServerError.getStatus());
}

private void keyNotInCache() {
throw new StorageException(Messages.KEY_NOT_PROVIDED.getKey(), Messages.KEY_NOT_PROVIDED.getStatus());
}
Expand Down Expand Up @@ -233,6 +266,6 @@ interface KeyOperation {

@FunctionalInterface
interface KeyValueOperation {
KeyValue storageRequest(String serviceId, KeyValue keyValue);
KeyValue storageRequest(String serviceId, KeyValue keyValue) throws StorageException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,15 @@
@Getter
public enum Messages {
DUPLICATE_KEY("org.zowe.apiml.cache.keyCollision", HttpStatus.CONFLICT),
DUPLICATE_VALUE("org.zowe.apiml.cache.duplicateValue", HttpStatus.CONFLICT),
KEY_NOT_PROVIDED("org.zowe.apiml.cache.keyNotProvided", HttpStatus.BAD_REQUEST),
KEY_NOT_IN_CACHE("org.zowe.apiml.cache.keyNotInCache", HttpStatus.NOT_FOUND),
INVALID_PAYLOAD("org.zowe.apiml.cache.invalidPayload", HttpStatus.BAD_REQUEST),
INSUFFICIENT_STORAGE("org.zowe.apiml.cache.insufficientStorage", HttpStatus.INSUFFICIENT_STORAGE),
PAYLOAD_TOO_LARGE("org.zowe.apiml.cache.payloadTooLarge", HttpStatus.BAD_REQUEST),
INTERNAL_SERVER_ERROR("org.zowe.apiml.common.internalRequestError", HttpStatus.INTERNAL_SERVER_ERROR),
MISSING_CERTIFICATE("org.zowe.apiml.cache.missingCertificate", HttpStatus.UNAUTHORIZED);
MISSING_CERTIFICATE("org.zowe.apiml.cache.missingCertificate", HttpStatus.UNAUTHORIZED),
INCOMPATIBLE_STORAGE_METHOD("org.zowe.apiml.cache.incompatibleStorageMethod", HttpStatus.BAD_REQUEST);
private final String key;
private final HttpStatus status;
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,23 @@ public interface Storage {
*/
KeyValue create(String serviceId, KeyValue toCreate);

/**
* Store new KeyValue pair in the storage. The entry will be stored in a list for a specific key.
*
* @param serviceId Id of the service to store the value for
* @param toCreate KeyValue pair to be created.
*/
KeyValue storeListItem(String serviceId, KeyValue toCreate) throws StorageException;

/**
* Return all the items in the list for a specific key.
*
* @param serviceId Id of the service to load all key/value pairs
* @param key key to lookup
* @return Map with the key/value pairs or null if there is none existing.
*/
Map<String, String> getAllMapItems(String serviceId, String key) throws StorageException;

/**
* Returns the keys associated with the provided keys.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.parsing.ConfigurationBuilderHolder;
import org.infinispan.configuration.parsing.ParserRegistry;
import org.infinispan.lock.EmbeddedClusteredLockManagerFactory;
import org.infinispan.lock.api.ClusteredLock;
import org.infinispan.lock.api.ClusteredLockManager;
import org.infinispan.manager.DefaultCacheManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
Expand Down Expand Up @@ -82,13 +85,23 @@ DefaultCacheManager cacheManager(ResourceLoader resourceLoader) {
cacheManager.administration()
.withFlags(CacheContainerAdmin.AdminFlag.VOLATILE)
.getOrCreateCache("zoweCache", builder.build());
cacheManager.administration()
.withFlags(CacheContainerAdmin.AdminFlag.VOLATILE)
.getOrCreateCache("zoweInvalidatedTokenCache", builder.build());
return cacheManager;
}

@Bean
public ClusteredLock lock(DefaultCacheManager cacheManager) {
ClusteredLockManager clm = EmbeddedClusteredLockManagerFactory.from(cacheManager);
clm.defineLock("zoweInvalidatedTokenLock");
return clm.get("zoweInvalidatedTokenLock");
}


@Bean
public Storage storage(DefaultCacheManager cacheManager) {
return new InfinispanStorage(cacheManager.getCache("zoweCache"));
public Storage storage(DefaultCacheManager cacheManager, ClusteredLock clusteredLock) {
return new InfinispanStorage(cacheManager.getCache("zoweCache"), cacheManager.getCache("zoweInvalidatedTokenCache"), clusteredLock);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,31 @@
package org.zowe.apiml.caching.service.infinispan.storage;

import lombok.extern.slf4j.Slf4j;
import org.infinispan.lock.api.ClusteredLock;
import org.zowe.apiml.caching.model.KeyValue;
import org.zowe.apiml.caching.service.Messages;
import org.zowe.apiml.caching.service.Storage;
import org.zowe.apiml.caching.service.StorageException;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

@Slf4j
public class InfinispanStorage implements Storage {


private final ConcurrentMap<String, KeyValue> cache;
private final ConcurrentMap<String, Map<String, String>> tokenCache;
private final ClusteredLock lock;

public InfinispanStorage(ConcurrentMap<String, KeyValue> cache) {
public InfinispanStorage(ConcurrentMap<String, KeyValue> cache, ConcurrentMap<String, Map<String, String>> tokenCache, ClusteredLock lock) {
this.cache = cache;
this.tokenCache = tokenCache;
this.lock = lock;
}

@Override
Expand All @@ -42,6 +50,47 @@ public KeyValue create(String serviceId, KeyValue toCreate) {
return null;
}

@Override
public KeyValue storeListItem(String serviceId, KeyValue toCreate) {
CompletableFuture<Boolean> complete = lock.tryLock(4, TimeUnit.SECONDS).whenComplete((r, ex) -> {
if (Boolean.TRUE.equals(r)) {
try {
String cacheKey = serviceId + "invalidTokens";
if (tokenCache.get(cacheKey) != null &&
tokenCache.get(cacheKey).containsKey(toCreate.getKey())) {
throw new StorageException(Messages.DUPLICATE_VALUE.getKey(), Messages.DUPLICATE_VALUE.getStatus(), toCreate.getValue());
}
log.info("Storing the invalidated token: {}|{}|{}", serviceId, toCreate.getKey(), toCreate.getValue());
Map<String, String> tokensList = tokenCache.get(cacheKey);
if (tokensList == null) {
tokensList = new HashMap<>();
}
tokensList.put(toCreate.getKey(), toCreate.getValue());
tokenCache.put(cacheKey, tokensList);
} finally {
lock.unlock();
}
}
});
try {
complete.join();
} catch (CompletionException e) {
if (e.getCause() instanceof StorageException) {
throw (StorageException) e.getCause();
} else {
log.error("Unexpected error while acquiring the lock ", e);
throw e;
}
}
return null;
}

@Override
public Map<String, String> getAllMapItems(String serviceId, String key) {
log.info("Reading all revoked tokens for service {} ", serviceId);
return tokenCache.get(serviceId + key);
}

@Override
public KeyValue read(String serviceId, String key) {
log.info("Reading record for service {} under key {}", serviceId, key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ public KeyValue create(String serviceId, KeyValue toCreate) {
return toCreate;
}

@Override
public KeyValue storeListItem(String serviceId, KeyValue toCreate) throws StorageException {
throw new StorageException(Messages.INCOMPATIBLE_STORAGE_METHOD.getKey(), Messages.INCOMPATIBLE_STORAGE_METHOD.getStatus());
}

@Override
public Map<String, String> getAllMapItems(String serviceId, String key) throws StorageException {
throw new StorageException(Messages.INCOMPATIBLE_STORAGE_METHOD.getKey(), Messages.INCOMPATIBLE_STORAGE_METHOD.getStatus());
}

@Override
public KeyValue read(String serviceId, String key) {
log.info("Reading Record: {}|{}|{}", serviceId, key, "-");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ public KeyValue create(String serviceId, KeyValue toCreate) {
return toCreate;
}

@Override
public KeyValue storeListItem(String serviceId, KeyValue toCreate) throws StorageException {
throw new StorageException(Messages.INCOMPATIBLE_STORAGE_METHOD.getKey(), Messages.INCOMPATIBLE_STORAGE_METHOD.getStatus());
}

@Override
public Map<String, String> getAllMapItems(String serviceId, String key) throws StorageException {
throw new StorageException(Messages.INCOMPATIBLE_STORAGE_METHOD.getKey(), Messages.INCOMPATIBLE_STORAGE_METHOD.getStatus());
}

@Override
@Retryable(value = RetryableRedisException.class)
public KeyValue read(String serviceId, String key) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Retryable;
import org.zowe.apiml.caching.model.KeyValue;
import org.zowe.apiml.caching.service.*;
import org.zowe.apiml.caching.service.EvictionStrategy;
import org.zowe.apiml.caching.service.Messages;
import org.zowe.apiml.caching.service.Storage;
import org.zowe.apiml.caching.service.StorageException;
import org.zowe.apiml.caching.service.vsam.config.VsamConfig;
import org.zowe.apiml.message.log.ApimlLogger;

Expand Down Expand Up @@ -91,6 +94,16 @@ public KeyValue create(String serviceId, KeyValue toCreate) {
return result;
}

@Override
public KeyValue storeListItem(String serviceId, KeyValue toCreate) throws StorageException {
throw new StorageException(Messages.INCOMPATIBLE_STORAGE_METHOD.getKey(), Messages.INCOMPATIBLE_STORAGE_METHOD.getStatus());
}

@Override
public Map<String, String> getAllMapItems(String serviceId, String key) throws StorageException {
throw new StorageException(Messages.INCOMPATIBLE_STORAGE_METHOD.getKey(), Messages.INCOMPATIBLE_STORAGE_METHOD.getStatus());
}

private boolean aboveThreshold(int currentSize) {
return currentSize >= vsamConfig.getGeneralConfig().getMaxDataSize();
}
Expand Down
14 changes: 14 additions & 0 deletions caching-service/src/main/resources/caching-log-messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ messages:
reason: "The storage space is full."
action: "Change the definition of the VSAM and the configuration for the maximum records stored or replace Reject strategy with Remove Oldest."

- key: org.zowe.apiml.cache.duplicateValue
number: ZWECS135
type: ERROR
text: "Adding value '%s' resulted in a collision in the cache."
reason: "Value is already in the cache."
action: "Provide a value that is not in the cache."

- key: org.zowe.apiml.cache.incompatibleStorageMethod
number: ZWECS136
type: ERROR
text: "The storage of list items is supported only on Infinispan."
reason: "This caching storage method doesn't support this API."
action: "Switch to Infinispan to be able to use this API."

# Storage messages (151 - 160)
- key: org.zowe.apiml.cache.errorInitializingStorage
number: ZWECS151
Expand Down
1 change: 1 addition & 0 deletions caching-service/src/main/resources/infinispan.xml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
<allow-list>
<class>org.zowe.apiml.caching.model.KeyValue</class>
<class>java.util.HashMap</class>
<class>java.util.Arrays$ArrayList</class>
</allow-list>
</serialization>

Expand Down
Loading

0 comments on commit c7f79d5

Please sign in to comment.