From 483bc1528aa54f285edaa75ad0e7c67af7240cc6 Mon Sep 17 00:00:00 2001 From: Sandor Molnar Date: Thu, 10 Mar 2022 21:27:47 +0100 Subject: [PATCH] CDPD-35799 KNOX-2712 - Managing custom Knox Token metadata (#542) Change-Id: I68b16236000dafda0ae627a98426d93948dca9d4 --- .../service/knoxtoken/TokenResource.java | 54 +++++++++++++++++-- .../knoxtoken/TokenServiceResourceTest.java | 17 +++++- .../services/security/token/KnoxToken.java | 8 +++ .../security/token/TokenMetadata.java | 18 +++++++ .../token-management/app/metadata.ts | 1 + .../app/token.management.component.html | 10 +++- .../app/token.management.component.ts | 8 +++ 7 files changed, 110 insertions(+), 6 deletions(-) diff --git a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java index 335a502fdc..58c87cf9e0 100644 --- a/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java +++ b/gateway-service-knoxtoken/src/main/java/org/apache/knox/gateway/service/knoxtoken/TokenResource.java @@ -29,11 +29,13 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Enumeration; import java.util.Map; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Optional; +import java.util.TreeSet; import java.util.UUID; import javax.annotation.PostConstruct; @@ -45,9 +47,9 @@ import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.Produces; -import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.KeyLengthException; @@ -118,6 +120,7 @@ public class TokenResource { private static final String LIFESPAN_INPUT_ENABLED_PARAM = "knox.token.lifespan.input.enabled"; private static final String LIFESPAN_INPUT_ENABLED_TEXT = "lifespanInputEnabled"; static final String KNOX_TOKEN_USER_LIMIT_EXCEEDED_ACTION = "knox.token.user.limit.exceeded.action"; + private static final String METADATA_QUERY_PARAM_PREFIX = "md_"; private static final long TOKEN_TTL_DEFAULT = 30000L; static final String TOKEN_API_PATH = "knoxtoken/api/v1"; static final String RESOURCE_PATH = TOKEN_API_PATH + "/token"; @@ -405,11 +408,43 @@ public Response doPost() { @GET @Path(GET_USER_TOKENS) @Produces({APPLICATION_JSON, APPLICATION_XML}) - public Response getUserTokens(@QueryParam("userName") String userName) { + public Response getUserTokens(@Context UriInfo uriInfo) { if (tokenStateService == null) { return Response.status(Response.Status.SERVICE_UNAVAILABLE).entity("{\n \"error\": \"Token management is not configured\"\n}\n").build(); } else { - final Collection tokens = tokenStateService.getTokens(userName); + if (uriInfo == null) { + throw new IllegalArgumentException("URI info cannot be NULL."); + } + final Map metadataMap = new HashMap<>(); + uriInfo.getQueryParameters().entrySet().forEach(entry -> { + if (entry.getKey().startsWith(METADATA_QUERY_PARAM_PREFIX)) { + String metadataName = entry.getKey().substring(METADATA_QUERY_PARAM_PREFIX.length()); + metadataMap.put(metadataName, entry.getValue().get(0)); + } + }); + + final String userName = uriInfo.getQueryParameters().getFirst("userName"); + final Collection userTokens = tokenStateService.getTokens(userName); + final Collection tokens = new TreeSet<>(); + if (metadataMap.isEmpty()) { + tokens.addAll(userTokens); + } else { + userTokens.forEach(knoxToken -> { + for (Map.Entry entry : metadataMap.entrySet()) { + if (StringUtils.isBlank(entry.getValue()) || "*".equals(entry.getValue())) { + // we should only filter tokens by metadata name + if (knoxToken.hasMetadata(entry.getKey())) { + tokens.add(knoxToken); + } + } else { + // metadata value should also match + if (entry.getValue().equals(knoxToken.getMetadataValue(entry.getKey()))) { + tokens.add(knoxToken); + } + } + } + }); + } return Response.status(Response.Status.OK).entity(JsonUtils.renderAsJsonString(Collections.singletonMap("tokens", tokens))).build(); } } @@ -740,6 +775,7 @@ private Response getAuthenticationToken() { final String comment = request.getParameter(COMMENT); final TokenMetadata tokenMetadata = new TokenMetadata(p.getName(), StringUtils.isBlank(comment) ? null : comment); tokenMetadata.setPasscode(tokenMAC.hash(tokenId, issueTime, p.getName(), passcode)); + addArbitraryTokenMetadata(tokenMetadata); tokenStateService.addMetadata(tokenId, tokenMetadata); log.storedToken(getTopologyName(), Tokens.getTokenDisplayText(accessToken), Tokens.getTokenIDDisplayText(tokenId)); } @@ -754,6 +790,18 @@ private Response getAuthenticationToken() { return Response.ok().entity("{ \"Unable to acquire token.\" }").build(); } + private void addArbitraryTokenMetadata(TokenMetadata tokenMetadata) { + final Enumeration paramNames = request.getParameterNames(); + while (paramNames.hasMoreElements()) { + final String paramName = paramNames.nextElement(); + if (paramName.startsWith(METADATA_QUERY_PARAM_PREFIX)) { + final String metadataName = paramName.substring(METADATA_QUERY_PARAM_PREFIX.length()); + final String metadataValue = request.getParameter(paramName); + tokenMetadata.add(metadataName, metadataValue); + } + } + } + private String generatePasscodeField(String tokenId, String passcode) { final String base64TokenIdPasscode = Base64.encodeBase64String(tokenId.getBytes(StandardCharsets.UTF_8)) + "::" + Base64.encodeBase64String(passcode.getBytes(StandardCharsets.UTF_8)); return Base64.encodeBase64String(base64TokenIdPasscode.getBytes(StandardCharsets.UTF_8)); diff --git a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java index 624c67c2da..f693980ce7 100644 --- a/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java +++ b/gateway-service-knoxtoken/src/test/java/org/apache/knox/gateway/service/knoxtoken/TokenServiceResourceTest.java @@ -64,7 +64,10 @@ import javax.security.auth.Subject; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.MultivaluedHashMap; +import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; import java.io.IOException; import java.security.KeyPair; @@ -152,6 +155,7 @@ private void configureCommonExpectations(Map contextExpectations if (contextExpectations.containsKey(TokenResource.LIFESPAN)) { EasyMock.expect(request.getParameter(TokenResource.LIFESPAN)).andReturn(contextExpectations.get(TokenResource.LIFESPAN)).anyTimes(); } + EasyMock.expect(request.getParameterNames()).andReturn(Collections.emptyEnumeration()).anyTimes(); GatewayServices services = EasyMock.createNiceMock(GatewayServices.class); EasyMock.expect(context.getAttribute(GatewayServices.GATEWAY_SERVICES_ATTRIBUTE)).andReturn(services).anyTimes(); @@ -1002,7 +1006,7 @@ public void testTokenLimitChangeAfterAlreadyHavingTokens() throws Exception { for (int i = 0; i < numberOfPreExistingTokens; i++) { tr.doGet(); } - Response getKnoxTokensResponse = tr.getUserTokens(USER_NAME); + Response getKnoxTokensResponse = getUserTokensResponse(tr); Collection tokens = ((Map>) JsonUtils.getObjectFromJsonString(getKnoxTokensResponse.getEntity().toString())) .get("tokens"); assertEquals(tokens.size(), numberOfPreExistingTokens); @@ -1016,6 +1020,15 @@ public void testTokenLimitChangeAfterAlreadyHavingTokens() throws Exception { assertTrue(response.getEntity().toString().contains("Unable to get token - token limit exceeded.")); } + private Response getUserTokensResponse(TokenResource tokenResource) { + final MultivaluedMap queryParameters = new MultivaluedHashMap<>(); + queryParameters.put("userName", Arrays.asList(USER_NAME)); + final UriInfo uriInfo = EasyMock.createNiceMock(UriInfo.class); + EasyMock.expect(uriInfo.getQueryParameters()).andReturn(queryParameters).anyTimes(); + EasyMock.replay(uriInfo); + return tokenResource.getUserTokens(uriInfo); + } + @Test public void testTokenLimitPerUserExceeded() throws Exception { try { @@ -1058,7 +1071,7 @@ private void testLimitingTokensPerUser(int configuredLimit, int numberOfTokens, throw new Exception(getTokenResponse.getEntity().toString()); } } - final Response getKnoxTokensResponse = tr.getUserTokens(USER_NAME); + final Response getKnoxTokensResponse = getUserTokensResponse(tr); final Collection tokens = ((Map>) JsonUtils.getObjectFromJsonString(getKnoxTokensResponse.getEntity().toString())) .get("tokens"); assertEquals(tokens.size(), revokeOldestToken ? configuredLimit : numberOfTokens); diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/KnoxToken.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/KnoxToken.java index 220c093a96..b34397be0e 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/KnoxToken.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/KnoxToken.java @@ -83,6 +83,14 @@ public void setMetadata(TokenMetadata metadata) { this.metadata = metadata; } + public String getMetadataValue(String key) { + return this.metadata == null ? null : metadata.getMetadata(key); + } + + public boolean hasMetadata(String key) { + return getMetadataValue(key) != null; + } + @Override public int compareTo(KnoxToken other) { return Long.compare(this.issueTime, other.issueTime); diff --git a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java index 1f612c5d71..8fa49cd563 100644 --- a/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java +++ b/gateway-spi/src/main/java/org/apache/knox/gateway/services/security/token/TokenMetadata.java @@ -16,7 +16,9 @@ */ package org.apache.knox.gateway.services.security.token; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; import java.util.Map; import org.apache.commons.lang3.StringUtils; @@ -33,6 +35,7 @@ public class TokenMetadata { public static final String COMMENT = "comment"; public static final String ENABLED = "enabled"; public static final String PASSCODE = "passcode"; + private static final List KNOWN_MD_NAMES = Arrays.asList(USER_NAME, COMMENT, ENABLED, PASSCODE); private final Map metadataMap = new HashMap<>(); @@ -66,6 +69,21 @@ public Map getMetadataMap() { return new HashMap(this.metadataMap); } + @JsonIgnore + public String getMetadata(String key) { + return this.metadataMap.get(key); + } + + public Map getCustomMetadataMap() { + final Map customMetadataMap = new HashMap<>(); + this.metadataMap.forEach((key, value) -> { + if (!KNOWN_MD_NAMES.contains(key)) { + customMetadataMap.put(key, value); + } + }); + return customMetadataMap; + } + public String getUserName() { return metadataMap.get(USER_NAME); } diff --git a/knox-token-management-ui/token-management/app/metadata.ts b/knox-token-management-ui/token-management/app/metadata.ts index 26c68849de..da5ff3fa94 100644 --- a/knox-token-management-ui/token-management/app/metadata.ts +++ b/knox-token-management-ui/token-management/app/metadata.ts @@ -19,4 +19,5 @@ export class Metadata { enabled: boolean; userName: string; comment: string; + customMetadataMap: Map; } diff --git a/knox-token-management-ui/token-management/app/token.management.component.html b/knox-token-management-ui/token-management/app/token.management.component.html index 1ba8070b0c..62932eada1 100644 --- a/knox-token-management-ui/token-management/app/token.management.component.html +++ b/knox-token-management-ui/token-management/app/token.management.component.html @@ -28,6 +28,7 @@ Issued Expires Comment + Additional Metadata Actions @@ -38,6 +39,13 @@ {{formatDateTime(knoxToken.expirationLong)}} {{formatDateTime(knoxToken.expirationLong)}} {{knoxToken.metadata.comment}} + +
    +
  • + {{metadata[0]}} = {{metadata[1]}} +
  • +
+ @@ -47,7 +55,7 @@ - + diff --git a/knox-token-management-ui/token-management/app/token.management.component.ts b/knox-token-management-ui/token-management/app/token.management.component.ts index fb926fce9c..15a0e8c7d1 100644 --- a/knox-token-management-ui/token-management/app/token.management.component.ts +++ b/knox-token-management-ui/token-management/app/token.management.component.ts @@ -80,4 +80,12 @@ export class TokenManagementComponent implements OnInit { return Date.now() > expiration; } + getCustomMetadataArray(knoxToken: KnoxToken): [string, string][] { + let mdMap = new Map(); + if (knoxToken.metadata.customMetadataMap) { + mdMap = knoxToken.metadata.customMetadataMap; + } + return Array.from(Object.entries(mdMap)); + } + }