You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@knox.apache.org by sm...@apache.org on 2022/03/10 20:27:55 UTC

[knox] branch master updated: KNOX-2712 - Managing custom Knox Token metadata (#542)

This is an automated email from the ASF dual-hosted git repository.

smolnar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/knox.git


The following commit(s) were added to refs/heads/master by this push:
     new 5b4040f  KNOX-2712 - Managing custom Knox Token metadata (#542)
5b4040f is described below

commit 5b4040ffeb6c0fee7a69b87ae83b54077b0c634e
Author: Sandor Molnar <sm...@apache.org>
AuthorDate: Thu Mar 10 21:27:47 2022 +0100

    KNOX-2712 - Managing custom Knox Token metadata (#542)
---
 .../gateway/service/knoxtoken/TokenResource.java   | 54 ++++++++++++++++++++--
 .../knoxtoken/TokenServiceResourceTest.java        | 17 ++++++-
 .../gateway/services/security/token/KnoxToken.java |  8 ++++
 .../services/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 96c189e..ea03439 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.time.format.DateTimeParseException;
 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;
@@ -46,9 +48,9 @@ import javax.ws.rs.POST;
 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;
@@ -119,6 +121,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 class TokenResource {
   @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<KnoxToken> tokens = tokenStateService.getTokens(userName);
+      if (uriInfo == null) {
+        throw new IllegalArgumentException("URI info cannot be NULL.");
+      }
+      final Map<String, String> 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<KnoxToken> userTokens = tokenStateService.getTokens(userName);
+      final Collection<KnoxToken> tokens = new TreeSet<>();
+      if (metadataMap.isEmpty()) {
+        tokens.addAll(userTokens);
+      } else {
+        userTokens.forEach(knoxToken -> {
+          for (Map.Entry<String, String> 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();
     }
   }
@@ -733,6 +768,7 @@ public class TokenResource {
           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));
         }
@@ -747,6 +783,18 @@ public class TokenResource {
     return Response.ok().entity("{ \"Unable to acquire token.\" }").build();
   }
 
+  private void addArbitraryTokenMetadata(TokenMetadata tokenMetadata) {
+    final Enumeration<String> 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 e74f2e2..eaf514f 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
@@ -65,7 +65,10 @@ import org.junit.Test;
 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;
@@ -153,6 +156,7 @@ public class TokenServiceResourceTest {
     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();
@@ -1007,7 +1011,7 @@ public class TokenServiceResourceTest {
     for (int i = 0; i < numberOfPreExistingTokens; i++) {
       tr.doGet();
     }
-    Response getKnoxTokensResponse = tr.getUserTokens(USER_NAME);
+    Response getKnoxTokensResponse = getUserTokensResponse(tr);
     Collection<String> tokens = ((Map<String, Collection<String>>) JsonUtils.getObjectFromJsonString(getKnoxTokensResponse.getEntity().toString()))
             .get("tokens");
     assertEquals(tokens.size(), numberOfPreExistingTokens);
@@ -1021,6 +1025,15 @@ public class TokenServiceResourceTest {
     assertTrue(response.getEntity().toString().contains("Unable to get token - token limit exceeded."));
   }
 
+  private Response getUserTokensResponse(TokenResource tokenResource) {
+    final MultivaluedMap<String, String> 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 {
@@ -1063,7 +1076,7 @@ public class TokenServiceResourceTest {
         throw new Exception(getTokenResponse.getEntity().toString());
       }
     }
-    final Response getKnoxTokensResponse = tr.getUserTokens(USER_NAME);
+    final Response getKnoxTokensResponse = getUserTokensResponse(tr);
     final Collection<String> tokens = ((Map<String, Collection<String>>) 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 220c093..b34397b 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 class KnoxToken implements Comparable<KnoxToken>{
     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 1f612c5..8fa49cd 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<String> KNOWN_MD_NAMES = Arrays.asList(USER_NAME, COMMENT, ENABLED, PASSCODE);
 
   private final Map<String, String> metadataMap = new HashMap<>();
 
@@ -66,6 +69,21 @@ public class TokenMetadata {
     return new HashMap<String, String>(this.metadataMap);
   }
 
+  @JsonIgnore
+  public String getMetadata(String key) {
+    return this.metadataMap.get(key);
+  }
+
+  public Map<String, String> getCustomMetadataMap() {
+    final Map<String, String> 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 26c6884..da5ff3f 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<string, string>;
 }
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 d08cdf4..4e5c6fd 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 @@
                 <th>Issued</th>
                 <th>Expires</th>
                 <th>Comment</th>
+                <th>Additional Metadata</th>
                 <th>Actions</th>
             </tr>
             </thead>
@@ -39,6 +40,13 @@
                 <td *ngIf="isTokenExpired(knoxToken.expirationLong)" style="color: red">{{formatDateTime(knoxToken.expirationLong)}}</td>
                 <td>{{knoxToken.metadata.comment}}</td>
                 <td>
+                  <ul>
+                    <li *ngFor="let metadata of getCustomMetadataArray(knoxToken)">
+                      {{metadata[0]}} = {{metadata[1]}}
+                    </li>
+                  </ul>
+                </td>
+                <td>
                     <button *ngIf="knoxToken.metadata.enabled && !isTokenExpired(knoxToken.expirationLong)" (click)="disableToken(knoxToken.tokenId);">Disable</button>
                     <button *ngIf="!knoxToken.metadata.enabled && !isTokenExpired(knoxToken.expirationLong)" (click)="enableToken(knoxToken.tokenId);">Enable</button>
                     <button (click)="revokeToken(knoxToken.tokenId);">Revoke</button>
@@ -47,7 +55,7 @@
             </tbody>
 		    <tfoot>
 		    <tr>
-		        <td colspan="5">
+		        <td colspan="6">
 		            <mfBootstrapPaginator [rowsOnPageSet]="[5,10,15]"></mfBootstrapPaginator>
 		        </td>
 		    </tr>
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 fb926fc..15a0e8c 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));
+    }
+
 }