diff --git a/components/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension/pom.xml b/components/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension/pom.xml
new file mode 100644
index 0000000000..104da8bfb2
--- /dev/null
+++ b/components/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension/pom.xml
@@ -0,0 +1,92 @@
+
+
+
+ apimgt-extensions
+ org.wso2.carbon.devicemgt
+ 5.0.7-SNAPSHOT
+
+
+ 4.0.0
+ org.wso2.carbon.apimgt.keymgt.extension
+ bundle
+ WSO2 Carbon - API Key Management
+ This module extends the API manager's key management.
+ http://wso2.org
+
+
+
+ commons-codec.wso2
+ commons-codec
+
+
+ org.wso2.carbon.apimgt
+ org.wso2.carbon.apimgt.keymgt
+ ${carbon.api.mgt.version}
+
+
+ org.wso2.carbon.devicemgt
+ org.wso2.carbon.device.mgt.core
+
+
+ org.wso2.carbon.devicemgt
+ org.wso2.carbon.device.mgt.common
+
+
+
+
+
+
+ org.apache.felix
+ maven-scr-plugin
+
+
+ org.apache.felix
+ maven-bundle-plugin
+ 1.4.0
+ true
+
+
+ ${project.artifactId}
+ ${project.artifactId}
+ ${carbon.device.mgt.version}
+ API Management Application Bundle
+
+
+
+ org.wso2.carbon.apimgt.keymgt.extension
+
+
+
+
+
+ org.jacoco
+ jacoco-maven-plugin
+
+ ${basedir}/target/coverage-reports/jacoco-unit.exec
+
+
+
+ jacoco-initialize
+
+ prepare-agent
+
+
+
+ jacoco-site
+ test
+
+ report
+
+
+ ${basedir}/target/coverage-reports/jacoco-unit.exec
+ ${basedir}/target/coverage-reports/site
+
+
+
+
+
+
+
+
diff --git a/components/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension/src/main/java/org/wso2/carbon/apimgt/keymgt/extension/KeyValidationHandler.java b/components/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension/src/main/java/org/wso2/carbon/apimgt/keymgt/extension/KeyValidationHandler.java
new file mode 100644
index 0000000000..50ee35526f
--- /dev/null
+++ b/components/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension/src/main/java/org/wso2/carbon/apimgt/keymgt/extension/KeyValidationHandler.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright (c) 2022, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
+ *
+ * WSO2 Inc. licenses this file to you 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 org.wso2.carbon.apimgt.keymgt.extension;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.wso2.carbon.apimgt.api.APIManagementException;
+import org.wso2.carbon.apimgt.api.model.AccessTokenInfo;
+import org.wso2.carbon.apimgt.api.model.KeyManager;
+import org.wso2.carbon.apimgt.api.model.subscription.URLMapping;
+import org.wso2.carbon.apimgt.impl.APIConstants;
+import org.wso2.carbon.apimgt.impl.caching.CacheProvider;
+import org.wso2.carbon.apimgt.impl.dto.APIKeyValidationInfoDTO;
+import org.wso2.carbon.apimgt.impl.dto.KeyManagerDto;
+import org.wso2.carbon.apimgt.impl.factory.KeyManagerHolder;
+import org.wso2.carbon.apimgt.keymgt.APIKeyMgtException;
+import org.wso2.carbon.apimgt.keymgt.SubscriptionDataHolder;
+import org.wso2.carbon.apimgt.keymgt.handlers.DefaultKeyValidationHandler;
+import org.wso2.carbon.apimgt.keymgt.model.SubscriptionDataStore;
+import org.wso2.carbon.apimgt.keymgt.model.entity.API;
+import org.wso2.carbon.apimgt.keymgt.service.TokenValidationContext;
+import org.wso2.carbon.context.PrivilegedCarbonContext;
+import org.wso2.carbon.device.mgt.common.permission.mgt.Permission;
+import org.wso2.carbon.device.mgt.common.permission.mgt.PermissionManagementException;
+import org.wso2.carbon.device.mgt.common.permission.mgt.PermissionManagerService;
+import org.wso2.carbon.device.mgt.core.permission.mgt.PermissionManagerServiceImpl;
+import org.wso2.carbon.user.api.UserRealm;
+import org.wso2.carbon.user.api.UserStoreException;
+import org.wso2.carbon.user.core.service.RealmService;
+import org.wso2.carbon.utils.multitenancy.MultitenantUtils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.StringTokenizer;
+
+public class KeyValidationHandler extends DefaultKeyValidationHandler {
+
+ /* This key validation handler is written extending API Manager's
+ * AbstractKeyValidationHandler, which implements KeyValidationHandler
+ * where all the methods have been implemented. Since the logic is
+ * taken from KeyValidationHandler, the latest logical changes
+ * should be monitored and updated here accordingly */
+
+ private static final Log log = LogFactory.getLog(KeyValidationHandler.class);
+
+ public KeyValidationHandler() {
+ log.info(this.getClass().getName() + " Initialised");
+ }
+
+ @Override
+ public boolean validateScopes(TokenValidationContext validationContext) throws APIKeyMgtException {
+
+ if (validationContext.isCacheHit()) {
+ return true;
+ }
+ APIKeyValidationInfoDTO apiKeyValidationInfoDTO = validationContext.getValidationInfoDTO();
+
+ if (apiKeyValidationInfoDTO == null) {
+ throw new APIKeyMgtException("Key Validation information not set");
+ }
+ String tenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain();
+ String httpVerb = validationContext.getHttpVerb();
+ String[] scopes;
+ Set scopesSet = apiKeyValidationInfoDTO.getScopes();
+ StringBuilder scopeList = new StringBuilder();
+
+ if (scopesSet != null && !scopesSet.isEmpty()) {
+ scopes = scopesSet.toArray(new String[scopesSet.size()]);
+ if (log.isDebugEnabled() && scopes != null) {
+ for (String scope : scopes) {
+ scopeList.append(scope);
+ scopeList.append(",");
+ }
+ scopeList.deleteCharAt(scopeList.length() - 1);
+ log.debug("Scopes allowed for token : " + validationContext.getAccessToken() + " : "
+ + scopeList.toString());
+ }
+ }
+
+ String resourceList = validationContext.getMatchingResource();
+ List resourceArray;
+ if ((APIConstants.GRAPHQL_QUERY.equalsIgnoreCase(validationContext.getHttpVerb()))
+ || (APIConstants.GRAPHQL_MUTATION.equalsIgnoreCase(validationContext.getHttpVerb()))
+ || (APIConstants.GRAPHQL_SUBSCRIPTION.equalsIgnoreCase(validationContext.getHttpVerb()))) {
+ resourceArray = new ArrayList<>(Arrays.asList(resourceList.split(",")));
+ } else {
+ resourceArray = new ArrayList<>(Arrays.asList(resourceList));
+ }
+
+ String actualVersion = validationContext.getVersion();
+ //Check if the api version has been prefixed with _default_
+ if (actualVersion != null && actualVersion.startsWith(APIConstants.DEFAULT_VERSION_PREFIX)) {
+ //Remove the prefix from the version.
+ actualVersion = actualVersion.split(APIConstants.DEFAULT_VERSION_PREFIX)[1];
+ }
+ SubscriptionDataStore tenantSubscriptionStore =
+ SubscriptionDataHolder.getInstance().getTenantSubscriptionStore(tenantDomain);
+ API api = tenantSubscriptionStore.getApiByContextAndVersion(validationContext.getContext(),
+ actualVersion);
+ boolean scopesValidated = false;
+ if (api != null) {
+
+ for (String resource : resourceArray) {
+ List resources = api.getResources();
+ URLMapping urlMapping = null;
+ for (URLMapping mapping : resources) {
+ if (Objects.equals(mapping.getHttpMethod(), httpVerb) || "WS".equalsIgnoreCase(api.getApiType())) {
+ if (isResourcePathMatching(resource, mapping)) {
+ urlMapping = mapping;
+ break;
+ }
+ }
+ }
+ if (urlMapping != null) {
+ if (urlMapping.getScopes().size() == 0) {
+ scopesValidated = true;
+ continue;
+ }
+ List mappingScopes = urlMapping.getScopes();
+ boolean validate = false;
+ for (String scope : mappingScopes) {
+ if (scopesSet.contains(scope)) {
+ scopesValidated = true;
+ validate = true;
+ break;
+ }
+ try {
+ validate = scopesValidated = authorizePermissions(validationContext);
+ break;
+ } catch (UserStoreException e) {
+ String msg = "Error occurred while validating user permissions";
+ log.error(msg, e);
+ throw new APIKeyMgtException(msg);
+ }
+ }
+ if (!validate && urlMapping.getScopes().size() > 0) {
+ break;
+ }
+ }
+ }
+ }
+ if (!scopesValidated) {
+ apiKeyValidationInfoDTO.setAuthorized(false);
+ apiKeyValidationInfoDTO.setValidationStatus(APIConstants.KeyValidationStatus.INVALID_SCOPE);
+ }
+ return scopesValidated;
+ }
+
+ /**
+ * Authorizes the permissions of a user for a given context
+ *
+ * @param validationContext token validation context object
+ * @return returns whether a user is authorized
+ * @throws UserStoreException throws if an error occurs while getting the tenant user realm
+ * */
+ private boolean authorizePermissions(TokenValidationContext validationContext) throws UserStoreException {
+ PrivilegedCarbonContext context = PrivilegedCarbonContext.getThreadLocalCarbonContext();
+ String username;
+
+ RealmService realmService = (RealmService) context.getOSGiService(RealmService.class, null);
+ UserRealm userRealm = realmService.getTenantUserRealm(PrivilegedCarbonContext
+ .getThreadLocalCarbonContext().getTenantId());;
+ AccessTokenInfo accessTokenInfo;
+ try {
+ accessTokenInfo = getAccessTokenInfo(validationContext);
+ } catch (APIManagementException e) {
+ log.error("Error occurred while getting access token info");
+ return false;
+ }
+ username = accessTokenInfo.getEndUserName();
+ String tenantAwareUsername = MultitenantUtils.getTenantAwareUsername(username);
+
+ List matchingPermissions;
+ StringBuilder ctx = new StringBuilder();
+ try {
+ PermissionManagerService permissionManagerService = PermissionManagerServiceImpl.getInstance();
+ String[] ctxArr = validationContext.getContext().split("/");
+ for (String c : ctxArr) {
+ if (c.matches("[v|V]\\d{1,3}\\.\\d{1,3}"))
+ ctx.append(c);
+ }
+ ctx = new StringBuilder(ctxArr[0] + "/" + ctxArr[1] + "/" + ctxArr[2] + "/" + ctxArr[3]);
+ matchingPermissions = permissionManagerService.getPermission(ctx.toString());
+ } catch (PermissionManagementException e) {
+ log.error("Error occurred while fetching permissions for context " + ctx, e);
+ return false;
+ }
+
+ String requestUri = validationContext.getContext();
+ String requestMethod = validationContext.getHttpVerb();
+ String contextPath = ctx.toString();
+
+ if (matchingPermissions == null) {
+ if (log.isDebugEnabled()) {
+ log.debug("Permission to request '" + requestUri + "' is not defined in the configuration");
+ }
+ return false;
+ }
+
+ String requiredPermission = null;
+ List matchingResources = new ArrayList<>();
+ for (Permission permission : matchingPermissions) {
+ if (requestMethod.equals(permission.getMethod()) && requestUri.matches(permission.getUrlPattern())) {
+ if (requestUri.equals(permission.getUrl())) { // is there a exact match
+ requiredPermission = permission.getPath();
+ break;
+ } else { // all templated urls add to a list
+ matchingResources.add(new MatchingResource(permission.getUrlPattern().replace(contextPath, ""), permission.getPath()));
+ }
+ }
+ }
+
+ if (requiredPermission == null) {
+ if (matchingResources.size() == 1) { // only 1 templated url found
+ requiredPermission = matchingResources.get(0).getPermission();
+ }
+
+ if (matchingResources.size() > 1) { // more than 1 templated urls found
+ String urlWithoutContext = requestUri.replace(contextPath, "");
+ StringTokenizer st = new StringTokenizer(urlWithoutContext, "/");
+ int tokenPosition = 1;
+ while (st.hasMoreTokens()) {
+ List tempList = new ArrayList<>();
+ String currentToken = st.nextToken();
+ for (MatchingResource matchingResource : matchingResources) {
+ StringTokenizer stmr = new StringTokenizer(matchingResource.getUrlPattern(), "/");
+ int internalTokenPosition = 1;
+ while (stmr.hasMoreTokens()) {
+ String internalToken = stmr.nextToken();
+ if ((tokenPosition == internalTokenPosition) && currentToken.equals(internalToken)) {
+ tempList.add(matchingResource);
+ }
+ internalTokenPosition++;
+ if (tokenPosition < internalTokenPosition) {
+ break;
+ }
+ }
+ }
+ if (tempList.size() == 1) {
+ requiredPermission = tempList.get(0).getPermission();
+ break;
+ }
+ tokenPosition++;
+ }
+ }
+ }
+
+ if (requiredPermission == null) {
+ if (log.isDebugEnabled()) {
+ log.debug("Matching permission not found for " + requestUri);
+ }
+ return false;
+ }
+
+ boolean isUserAuthorized;
+ try {
+ isUserAuthorized = userRealm.getAuthorizationManager().isUserAuthorized(
+ tenantAwareUsername,
+ requiredPermission,
+ "ui.execute" // check against null values
+ );
+ return isUserAuthorized;
+ } catch (Exception e) {
+ log.error("Error occurred while retrieving user store. " + e.getMessage());
+ return false;
+ }
+ }
+
+ private AccessTokenInfo getAccessTokenInfo(TokenValidationContext validationContext)
+ throws APIManagementException {
+
+ Object cachedAccessTokenInfo =
+ CacheProvider.createIntrospectionCache().get(validationContext.getAccessToken());
+ if (cachedAccessTokenInfo != null) {
+ return (AccessTokenInfo) cachedAccessTokenInfo;
+ }
+ String electedKeyManager = null;
+ // Obtaining details about the token.
+ if (StringUtils.isNotEmpty(validationContext.getTenantDomain())) {
+ Map
+ tenantKeyManagers = KeyManagerHolder.getTenantKeyManagers(validationContext.getTenantDomain());
+ KeyManager keyManagerInstance = null;
+ if (tenantKeyManagers.values().size() == 1) {
+ Map.Entry entry = tenantKeyManagers.entrySet().iterator().next();
+ if (entry != null) {
+ KeyManagerDto keyManagerDto = entry.getValue();
+ if (keyManagerDto != null && (validationContext.getKeyManagers()
+ .contains(APIConstants.KeyManager.API_LEVEL_ALL_KEY_MANAGERS) ||
+ validationContext.getKeyManagers().contains(keyManagerDto.getName()))) {
+ keyManagerInstance = keyManagerDto.getKeyManager();
+ electedKeyManager = entry.getKey();
+ }
+ }
+ } else if (tenantKeyManagers.values().size() > 1) {
+ if (validationContext.getKeyManagers()
+ .contains(APIConstants.KeyManager.API_LEVEL_ALL_KEY_MANAGERS)) {
+ for (Map.Entry keyManagerDtoEntry : tenantKeyManagers.entrySet()) {
+ if (keyManagerDtoEntry.getValue().getKeyManager() != null &&
+ keyManagerDtoEntry.getValue().getKeyManager()
+ .canHandleToken(validationContext.getAccessToken())) {
+ keyManagerInstance = keyManagerDtoEntry.getValue().getKeyManager();
+ electedKeyManager = keyManagerDtoEntry.getKey();
+ break;
+ }
+ }
+ } else {
+ for (String selectedKeyManager : validationContext.getKeyManagers()) {
+ KeyManagerDto keyManagerDto = tenantKeyManagers.get(selectedKeyManager);
+ if (keyManagerDto != null && keyManagerDto.getKeyManager() != null &&
+ keyManagerDto.getKeyManager().canHandleToken(validationContext.getAccessToken())) {
+ keyManagerInstance = keyManagerDto.getKeyManager();
+ electedKeyManager = selectedKeyManager;
+ break;
+ }
+ }
+ }
+ }
+
+ if (keyManagerInstance != null) {
+ AccessTokenInfo tokenInfo = keyManagerInstance.getTokenMetaData(validationContext.getAccessToken());
+ tokenInfo.setKeyManager(electedKeyManager);
+ CacheProvider.getGatewayIntrospectCache().put(validationContext.getAccessToken(), tokenInfo);
+ return tokenInfo;
+ } else {
+ log.debug("KeyManager not available to authorize token.");
+ }
+ }
+ return null;
+ }
+
+ private boolean isResourcePathMatching(String resourceString, URLMapping urlMapping) {
+
+ String resource = resourceString.trim();
+ String urlPattern = urlMapping.getUrlPattern().trim();
+
+ if (resource.equalsIgnoreCase(urlPattern)) {
+ return true;
+ }
+
+ // If the urlPattern is only one character longer than the resource and the urlPattern ends with a '/'
+ if (resource.length() + 1 == urlPattern.length() && urlPattern.endsWith("/")) {
+ // Check if resource is equal to urlPattern if the trailing '/' of the urlPattern is ignored
+ String urlPatternWithoutSlash = urlPattern.substring(0, urlPattern.length() - 1);
+ return resource.equalsIgnoreCase(urlPatternWithoutSlash);
+ }
+
+ return false;
+ }
+}
diff --git a/components/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension/src/main/java/org/wso2/carbon/apimgt/keymgt/extension/MatchingResource.java b/components/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension/src/main/java/org/wso2/carbon/apimgt/keymgt/extension/MatchingResource.java
new file mode 100644
index 0000000000..1fe0723800
--- /dev/null
+++ b/components/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension/src/main/java/org/wso2/carbon/apimgt/keymgt/extension/MatchingResource.java
@@ -0,0 +1,27 @@
+package org.wso2.carbon.apimgt.keymgt.extension;
+
+public class MatchingResource {
+ private String urlPattern;
+ private String permission;
+
+ public MatchingResource(String urlPattern, String permission) {
+ this.urlPattern = urlPattern;
+ this.permission = permission;
+ }
+
+ public String getUrlPattern() {
+ return urlPattern;
+ }
+
+ public void setUrlPattern(String urlPattern) {
+ this.urlPattern = urlPattern;
+ }
+
+ public String getPermission() {
+ return permission;
+ }
+
+ public void setPermission(String permission) {
+ this.permission = permission;
+ }
+}
diff --git a/components/apimgt-extensions/pom.xml b/components/apimgt-extensions/pom.xml
index b4eb8bbff7..d72878e226 100644
--- a/components/apimgt-extensions/pom.xml
+++ b/components/apimgt-extensions/pom.xml
@@ -37,6 +37,7 @@
org.wso2.carbon.apimgt.application.extension
org.wso2.carbon.apimgt.application.extension.api
org.wso2.carbon.apimgt.annotations
+ org.wso2.carbon.apimgt.keymgt.extension
diff --git a/features/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension.feature/pom.xml b/features/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension.feature/pom.xml
new file mode 100644
index 0000000000..921d92cc0d
--- /dev/null
+++ b/features/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension.feature/pom.xml
@@ -0,0 +1,106 @@
+
+
+
+
+
+ carbon-devicemgt
+ org.wso2.carbon.devicemgt
+ 5.0.7-SNAPSHOT
+ ../pom.xml
+
+
+ 4.0.0
+ org.wso2.carbon.apimgt.keymgt.extension.feature
+ pom
+ WSO2 Carbon - Api Key Mgt Extensions Feature
+ http://wso2.org
+ This feature contains apimgt related key management extensions
+
+
+
+ org.wso2.carbon.devicemgt
+ org.wso2.carbon.apimgt.keymgt.extension
+ ${carbon.device.mgt.version}
+
+
+
+
+
+
+ maven-resources-plugin
+ 2.6
+
+
+ copy-resources
+ generate-resources
+
+ copy-resources
+
+
+ src/main/resources
+
+
+ resources
+
+ build.properties
+ p2.inf
+
+
+
+
+
+
+
+
+ org.wso2.maven
+ carbon-p2-plugin
+ ${carbon.p2.plugin.version}
+
+
+ p2-feature-generation
+ package
+
+ p2-feature-gen
+
+
+ org.wso2.carbon.apimgt.keymgt.extension
+ ../../../features/etc/feature.properties
+
+
+ org.wso2.carbon.p2.category.type:server
+ org.eclipse.equinox.p2.type.group:false
+
+
+
+
+ org.wso2.carbon.devicemgt:org.wso2.carbon.apimgt.keymgt.extension:${carbon.device.mgt.version}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/features/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension.feature/src/main/resources/build.properties b/features/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension.feature/src/main/resources/build.properties
new file mode 100644
index 0000000000..9c86577d76
--- /dev/null
+++ b/features/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension.feature/src/main/resources/build.properties
@@ -0,0 +1 @@
+custom = true
diff --git a/features/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension.feature/src/main/resources/p2.inf b/features/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension.feature/src/main/resources/p2.inf
new file mode 100644
index 0000000000..a828a69832
--- /dev/null
+++ b/features/apimgt-extensions/org.wso2.carbon.apimgt.keymgt.extension.feature/src/main/resources/p2.inf
@@ -0,0 +1 @@
+instructions.configure = \
diff --git a/features/apimgt-extensions/pom.xml b/features/apimgt-extensions/pom.xml
index b035fd4275..ea046da07c 100644
--- a/features/apimgt-extensions/pom.xml
+++ b/features/apimgt-extensions/pom.xml
@@ -36,6 +36,7 @@
org.wso2.carbon.apimgt.webapp.publisher.feature
org.wso2.carbon.apimgt.application.extension.feature
+ org.wso2.carbon.apimgt.keymgt.extension.feature